Skip to main content

ryo_query_language/
converter.rs

1//! RyoQL Query → DiscoveryQuery 変換層
2//!
3//! RyoQLのYAML/JSON形式クエリを、ryo-analysisの内部クエリ形式に変換する。
4
5use crate::schema::{
6    GenericsMatch, MatchAttrs, NameMatcher, NameMatcherDetailed, Query, QueryKind, ReceiverKind,
7    Scope, Visibility,
8};
9use ryo_analysis::{DiscoveryQuery, Pattern, SymbolKind, TypeFilter};
10use thiserror::Error;
11
12/// 変換エラー
13#[derive(Debug, Error)]
14pub enum ConvertError {
15    /// 正規表現パターンが invalid。
16    #[error("invalid regex pattern: {0}")]
17    InvalidRegex(String),
18
19    /// `DiscoveryQuery` に変換できない `QueryKind`。
20    #[error("unsupported query kind for conversion: {kind:?}")]
21    UnsupportedKind {
22        /// 変換対象の `QueryKind`。
23        kind: QueryKind,
24    },
25
26    /// Pattern クエリに `name` field が無い。
27    #[error("Pattern query requires 'name' field")]
28    PatternNameRequired,
29
30    /// Or/And クエリに `queries` field が無い。
31    #[error("Or/And query requires 'queries' field")]
32    QueriesRequired,
33}
34
35/// クエリ変換器
36pub struct QueryConverter;
37
38impl QueryConverter {
39    /// RyoQL Query → DiscoveryQuery 変換
40    ///
41    /// # Note
42    /// 以下の機能はDiscoveryQueryでは未対応のため、後処理が必要:
43    /// - `inner` 条件 (ReturnType, Parameter, Field等)
44    /// - `match.is_async`, `match.is_unsafe` 等の詳細フィルタ
45    /// - `match.receiver` (ReceiverKind)
46    /// - `match.attributes`
47    /// - `match.generics`
48    /// - `scope.path`, `scope.exclude_path` (ファイルパスフィルタ)
49    /// - `resolve` (LSP連携)
50    pub fn to_discovery_query(query: &Query) -> Result<ConversionResult, ConvertError> {
51        match query.kind {
52            QueryKind::Or | QueryKind::And => {
53                // Or/Andは個別クエリに分解して返す
54                if query.queries.is_empty() {
55                    return Err(ConvertError::QueriesRequired);
56                }
57                let sub_results: Result<Vec<_>, _> =
58                    query.queries.iter().map(Self::to_discovery_query).collect();
59                Ok(ConversionResult {
60                    discovery_query: None,
61                    composite: Some(CompositeQuery {
62                        op: if query.kind == QueryKind::Or {
63                            CompositeOp::Or
64                        } else {
65                            CompositeOp::And
66                        },
67                        queries: sub_results?,
68                    }),
69                    post_filters: vec![],
70                    unsupported: vec![],
71                })
72            }
73            QueryKind::Pattern => {
74                // TODO: Pattern検索はryo-analysisのPattern機能と連携
75                // 現状は名前でパターン検索
76                let name = query
77                    .name
78                    .as_ref()
79                    .ok_or(ConvertError::PatternNameRequired)?;
80                Ok(ConversionResult {
81                    discovery_query: Some(DiscoveryQuery::exact(name)),
82                    composite: None,
83                    post_filters: vec![PostFilter::PatternSearch(name.clone())],
84                    unsupported: vec!["Pattern search (requires pattern registry)".to_string()],
85                })
86            }
87            _ => Self::convert_simple_query(query),
88        }
89    }
90
91    /// シンプルなクエリの変換
92    fn convert_simple_query(query: &Query) -> Result<ConversionResult, ConvertError> {
93        let mut unsupported = Vec::new();
94        let mut post_filters = Vec::new();
95
96        // Pattern変換
97        let pattern = Self::convert_name_matcher(query.r#match.as_ref())?;
98
99        // DiscoveryQuery構築
100        let mut dq = match pattern {
101            Some(p) => DiscoveryQuery::symbol(p.as_str()),
102            None => DiscoveryQuery::symbol("*"),
103        };
104
105        // Kind変換
106        if let Some(kinds) = Self::convert_kind(query.kind) {
107            dq = dq.kinds(kinds);
108        }
109
110        // Scope変換
111        if let Some(ref scope) = query.scope {
112            dq = Self::apply_scope(dq, scope, &mut post_filters, &mut unsupported);
113        }
114
115        // Limit
116        if let Some(limit) = query.limit {
117            dq = dq.limit(limit);
118        }
119
120        // Inner conditions: convert to PostFilters for DetailStore-based filtering
121        // (TypeFilter is for TypeFlowGraph, PostFilter is for DetailStore)
122        if !query.inner.is_empty() {
123            Self::collect_inner_post_filters(&query.inner, &mut post_filters)?;
124        }
125
126        // TypeFilter構築(generics.bounds のみ)
127        // Note: inner条件はPostFilterで処理するため、TypeFilterからは除外
128        let mut type_filter: Option<TypeFilter> = None;
129
130        // generics.bounds → TypeFilter.has_bound
131        if let Some(ref attrs) = query.r#match {
132            if let Some(ref generics) = attrs.generics {
133                if let Some(ref bounds) = generics.bounds {
134                    if let Some(bound_pattern) = Self::convert_bounds_to_pattern(bounds)? {
135                        let filter = type_filter.get_or_insert_with(TypeFilter::default);
136                        filter.has_bound = Some(bound_pattern);
137                    }
138                }
139            }
140        }
141
142        // TypeFilterをDiscoveryQueryに設定
143        if let Some(filter) = type_filter {
144            dq = dq.with_type_filter(filter);
145        }
146
147        // MatchAttrs の詳細フィルタ(DiscoveryQuery未対応 → 後処理)
148        if let Some(ref attrs) = query.r#match {
149            Self::collect_post_filters(attrs, &mut post_filters, &mut unsupported);
150        }
151
152        // Body pattern matching: 後処理フィルタとして追加
153        if let Some(ref body) = query.body {
154            post_filters.push(PostFilter::BodyMatch(body.clone()));
155        }
156
157        // Relations filter: 後処理フィルタとして追加
158        if let Some(ref relations) = query.relations {
159            if !relations.is_empty() {
160                post_filters.push(PostFilter::Relations(relations.clone()));
161            }
162        }
163
164        // resolve: Executor側で処理(CodeGraph使用)
165        // converter では何もしない
166
167        Ok(ConversionResult {
168            discovery_query: Some(dq),
169            composite: None,
170            post_filters,
171            unsupported,
172        })
173    }
174
175    /// NameMatcher → Pattern 変換
176    fn convert_name_matcher(attrs: Option<&MatchAttrs>) -> Result<Option<Pattern>, ConvertError> {
177        let attrs = match attrs {
178            Some(a) => a,
179            None => return Ok(None),
180        };
181
182        // name が指定されている場合はそれを優先
183        if let Some(ref name) = attrs.name {
184            let pattern = match name {
185                NameMatcher::Exact(s) => Pattern::exact(s),
186                NameMatcher::Detailed(d) => Self::convert_detailed_matcher(d)?,
187            };
188            return Ok(Some(pattern));
189        }
190
191        // name がない場合、pattern をショートハンドとして使用
192        if let Some(ref pattern_str) = attrs.pattern {
193            return Ok(Some(Pattern::glob(pattern_str)));
194        }
195
196        Ok(None)
197    }
198
199    /// 詳細NameMatcher → Pattern 変換
200    fn convert_detailed_matcher(d: &NameMatcherDetailed) -> Result<Pattern, ConvertError> {
201        // 優先順位: regex > glob > contains > starts_with > ends_with
202        if let Some(ref regex) = d.regex {
203            return Pattern::regex(regex).map_err(|e| ConvertError::InvalidRegex(e.to_string()));
204        }
205
206        if let Some(ref glob) = d.glob {
207            return Ok(Pattern::glob(glob));
208        }
209
210        if let Some(ref contains) = d.contains {
211            return Ok(Pattern::glob(format!("*{}*", contains)));
212        }
213
214        if let Some(ref starts) = d.starts_with {
215            return Ok(Pattern::glob(format!("{}*", starts)));
216        }
217
218        if let Some(ref ends) = d.ends_with {
219            return Ok(Pattern::glob(format!("*{}", ends)));
220        }
221
222        // 全部Noneの場合
223        Ok(Pattern::glob("*"))
224    }
225
226    /// QueryKind → SymbolKind 変換
227    fn convert_kind(kind: QueryKind) -> Option<Vec<SymbolKind>> {
228        match kind {
229            // Any: 全種類を検索
230            QueryKind::Any => None,
231            QueryKind::Function => Some(vec![SymbolKind::Function]),
232            QueryKind::Struct => Some(vec![SymbolKind::Struct]),
233            QueryKind::Enum => Some(vec![SymbolKind::Enum]),
234            QueryKind::Trait => Some(vec![SymbolKind::Trait]),
235            QueryKind::Impl => Some(vec![SymbolKind::Impl]),
236            QueryKind::Mod => Some(vec![SymbolKind::Mod]),
237            QueryKind::Const => Some(vec![SymbolKind::Const]),
238            QueryKind::Static => Some(vec![SymbolKind::Static]),
239            QueryKind::TypeAlias => Some(vec![SymbolKind::TypeAlias]),
240            // inner用/複合は単体では変換しない
241            QueryKind::ReturnType
242            | QueryKind::Parameter
243            | QueryKind::Field
244            | QueryKind::Variant
245            | QueryKind::Or
246            | QueryKind::And
247            | QueryKind::Pattern => None,
248            // Literalは特殊クエリ(シンボル検索ではない)
249            QueryKind::Literal => None,
250        }
251    }
252
253    /// Scope適用
254    fn apply_scope(
255        mut dq: DiscoveryQuery,
256        scope: &Scope,
257        post_filters: &mut Vec<PostFilter>,
258        _unsupported: &mut Vec<String>,
259    ) -> DiscoveryQuery {
260        // module → DiscoveryQuery.in_module
261        if let Some(ref module) = scope.module {
262            dq = dq.in_module(module);
263        }
264
265        // path, exclude_path → 後処理フィルタ
266        // Note: ファイルパスベースのフィルタは scope.module (SymbolPathベース) で代替可能。
267        // 互換性のため残すが、module の使用を推奨。
268        if let Some(ref path) = scope.path {
269            post_filters.push(PostFilter::PathInclude(path.clone()));
270        }
271
272        if let Some(ref exclude) = scope.exclude_path {
273            post_filters.push(PostFilter::PathExclude(exclude.clone()));
274        }
275
276        dq
277    }
278
279    /// Generics.bounds → Pattern 変換
280    ///
281    /// 複数のboundsがある場合は最初の一つを使用(将来的にはOR検索に対応)
282    fn convert_bounds_to_pattern(bounds: &[NameMatcher]) -> Result<Option<Pattern>, ConvertError> {
283        if bounds.is_empty() {
284            return Ok(None);
285        }
286
287        // 最初のbound matc herをPatternに変換
288        let first = &bounds[0];
289        let pattern = match first {
290            NameMatcher::Exact(s) => Pattern::exact(s),
291            NameMatcher::Detailed(d) => Self::convert_detailed_matcher(d)?,
292        };
293
294        Ok(Some(pattern))
295    }
296
297    /// Inner条件 → PostFilter 変換
298    ///
299    /// inner条件のQueryKindに応じてPostFilterを収集する:
300    /// - ReturnType → PostFilter::ReturnType (DetailStore.function().return_type で処理)
301    /// - Parameter → PostFilter::ParamType (DetailStore.function().params で処理)
302    /// - Field → PostFilter::FieldType (DetailStore.struct_().fields で処理)
303    fn collect_inner_post_filters(
304        inner: &[Query],
305        post_filters: &mut Vec<PostFilter>,
306    ) -> Result<(), ConvertError> {
307        for q in inner {
308            // Get the pattern string from match.name
309            let pattern_str = if let Some(ref attrs) = q.r#match {
310                if let Some(ref name) = attrs.name {
311                    Self::name_matcher_to_pattern_str(name)
312                } else {
313                    continue;
314                }
315            } else {
316                continue;
317            };
318
319            match q.kind {
320                QueryKind::ReturnType => {
321                    post_filters.push(PostFilter::ReturnType(pattern_str));
322                }
323                QueryKind::Parameter => {
324                    post_filters.push(PostFilter::ParamType(pattern_str));
325                }
326                QueryKind::Field => {
327                    post_filters.push(PostFilter::FieldType(pattern_str));
328                }
329                QueryKind::Variant => {
330                    // Variantもfield_typeとして扱う
331                    post_filters.push(PostFilter::FieldType(pattern_str));
332                }
333                _ => {
334                    // 他のQueryKindはinner条件として無効
335                }
336            }
337        }
338
339        Ok(())
340    }
341
342    /// NameMatcher → パターン文字列変換
343    fn name_matcher_to_pattern_str(name: &NameMatcher) -> String {
344        match name {
345            NameMatcher::Exact(s) => s.clone(),
346            NameMatcher::Detailed(d) => {
347                // Priority: glob > regex > contains > starts_with > ends_with
348                if let Some(ref glob) = d.glob {
349                    glob.clone()
350                } else if let Some(ref regex) = d.regex {
351                    format!("regex:{}", regex)
352                } else if let Some(ref contains) = d.contains {
353                    format!("*{}*", contains)
354                } else if let Some(ref starts_with) = d.starts_with {
355                    format!("{}*", starts_with)
356                } else if let Some(ref ends_with) = d.ends_with {
357                    format!("*{}", ends_with)
358                } else {
359                    "*".to_string()
360                }
361            }
362        }
363    }
364
365    /// Inner条件 → TypeFilter 変換 (deprecated, kept for backward compatibility)
366    #[allow(dead_code)]
367    fn convert_inner_to_type_filter(inner: &[Query]) -> Result<Option<TypeFilter>, ConvertError> {
368        let mut filter = TypeFilter::default();
369        let mut has_filter = false;
370
371        for q in inner {
372            let pattern = Self::convert_name_matcher(q.r#match.as_ref())?;
373            let Some(pattern) = pattern else {
374                continue;
375            };
376
377            match q.kind {
378                QueryKind::ReturnType => {
379                    filter.return_type = Some(pattern);
380                    has_filter = true;
381                }
382                QueryKind::Parameter => {
383                    filter.param_type = Some(pattern);
384                    has_filter = true;
385                }
386                QueryKind::Field => {
387                    filter.field_type = Some(pattern);
388                    has_filter = true;
389                }
390                QueryKind::Variant => {
391                    // Variantもfield_typeとして扱う(VariantFieldコンテキストで処理)
392                    filter.field_type = Some(pattern);
393                    has_filter = true;
394                }
395                _ => {
396                    // 他のQueryKindはinner条件として無効
397                }
398            }
399        }
400
401        if has_filter {
402            Ok(Some(filter))
403        } else {
404            Ok(None)
405        }
406    }
407
408    /// MatchAttrsから後処理フィルタを収集
409    fn collect_post_filters(
410        attrs: &MatchAttrs,
411        post_filters: &mut Vec<PostFilter>,
412        unsupported: &mut Vec<String>,
413    ) {
414        // TODO: これらのフィルタはDiscoveryQueryで直接サポートすべき
415        // 現状は後処理で対応
416
417        if let Some(ref sid) = attrs.symbol_id {
418            post_filters.push(PostFilter::SymbolId(sid.clone()));
419        }
420
421        if let Some(is_async) = attrs.is_async {
422            post_filters.push(PostFilter::IsAsync(is_async));
423            unsupported.push(format!("is_async: {}", is_async));
424        }
425
426        if let Some(is_unsafe) = attrs.is_unsafe {
427            post_filters.push(PostFilter::IsUnsafe(is_unsafe));
428            unsupported.push(format!("is_unsafe: {}", is_unsafe));
429        }
430
431        if let Some(ref vis) = attrs.vis {
432            post_filters.push(PostFilter::Visibility(vis.clone()));
433            unsupported.push(format!("visibility: {:?}", vis));
434        }
435
436        if let Some(ref receiver) = attrs.receiver {
437            post_filters.push(PostFilter::Receiver(*receiver));
438            unsupported.push(format!("receiver: {:?}", receiver));
439        }
440
441        if let Some(ref attributes) = attrs.attributes {
442            post_filters.push(PostFilter::Attributes(attributes.clone()));
443            unsupported.push(format!("attributes: {:?}", attributes));
444        }
445
446        if let Some(ref generics) = attrs.generics {
447            // bounds はTypeFilter.has_boundで処理するため除外
448            // params と lifetimes のみを PostFilter で処理
449            let has_params = generics.params.as_ref().is_some_and(|p| !p.is_empty());
450            let has_lifetimes = generics.lifetimes.as_ref().is_some_and(|l| !l.is_empty());
451
452            if has_params || has_lifetimes {
453                let filtered_generics = GenericsMatch {
454                    params: generics.params.clone(),
455                    bounds: None, // boundsはTypeFilterで処理
456                    lifetimes: generics.lifetimes.clone(),
457                };
458                post_filters.push(PostFilter::Generics(filtered_generics));
459                unsupported.push(format!(
460                    "generics (params/lifetimes): params={:?}, lifetimes={:?}",
461                    generics.params, generics.lifetimes
462                ));
463            }
464        }
465
466        // on_empty は executor で処理
467        if attrs.on_empty.is_some() {
468            post_filters.push(PostFilter::OnEmpty);
469        }
470
471        // parent フィルタ(Variant, Field, Method用)
472        if let Some(ref parent) = attrs.parent {
473            let pattern = match parent {
474                NameMatcher::Exact(s) => Pattern::exact(s),
475                NameMatcher::Detailed(d) => {
476                    // 優先順位: regex > glob > contains > starts_with > ends_with
477                    if let Some(ref regex) = d.regex {
478                        Pattern::regex(regex).unwrap_or_else(|_| Pattern::glob(regex))
479                    } else if let Some(ref glob) = d.glob {
480                        Pattern::glob(glob)
481                    } else if let Some(ref contains) = d.contains {
482                        Pattern::glob(format!("*{}*", contains))
483                    } else if let Some(ref starts) = d.starts_with {
484                        Pattern::glob(format!("{}*", starts))
485                    } else if let Some(ref ends) = d.ends_with {
486                        Pattern::glob(format!("*{}", ends))
487                    } else {
488                        Pattern::glob("*")
489                    }
490                }
491            };
492            post_filters.push(PostFilter::Parent(pattern));
493        }
494    }
495}
496
497/// 変換結果
498#[derive(Debug, Clone)]
499pub struct ConversionResult {
500    /// 変換されたDiscoveryQuery(Or/Andの場合はNone)
501    pub discovery_query: Option<DiscoveryQuery>,
502
503    /// 複合クエリ(Or/And)
504    pub composite: Option<CompositeQuery>,
505
506    /// 後処理が必要なフィルタ
507    pub post_filters: Vec<PostFilter>,
508
509    /// DiscoveryQuery未対応の機能リスト(警告用)
510    pub unsupported: Vec<String>,
511}
512
513/// 複合クエリ
514#[derive(Debug, Clone)]
515pub struct CompositeQuery {
516    /// 子クエリ群を結合する演算子。
517    pub op: CompositeOp,
518    /// 結合対象の子変換結果。
519    pub queries: Vec<ConversionResult>,
520}
521
522/// 複合演算子
523#[derive(Debug, Clone, Copy, PartialEq, Eq)]
524pub enum CompositeOp {
525    /// 和集合 (いずれかにマッチ)。
526    Or,
527    /// 積集合 (すべてにマッチ)。
528    And,
529}
530
531/// 後処理フィルタ
532///
533/// DiscoveryQueryでは表現できない条件。
534/// Executorで結果に対して後処理を行う。
535#[derive(Debug, Clone)]
536pub enum PostFilter {
537    /// async関数フィルタ
538    IsAsync(bool),
539    /// unsafe関数フィルタ
540    IsUnsafe(bool),
541    /// 可視性フィルタ
542    Visibility(Visibility),
543    /// レシーバーフィルタ
544    Receiver(ReceiverKind),
545    /// アトリビュートフィルタ
546    Attributes(Vec<String>),
547    /// ジェネリクスフィルタ
548    Generics(GenericsMatch),
549    /// パスincludeフィルタ
550    PathInclude(String),
551    /// パスexcludeフィルタ
552    PathExclude(String),
553    /// パターン検索
554    PatternSearch(String),
555    /// on_emptyリカバリー(executor処理)
556    OnEmpty,
557    /// 戻り値型フィルタ (inner: ReturnType)
558    ReturnType(String),
559    /// パラメータ型フィルタ (inner: Parameter)
560    ParamType(String),
561    /// フィールド型フィルタ (inner: Field)
562    FieldType(String),
563    /// 親シンボルフィルタ (Variant, Field, Method用)
564    Parent(Pattern),
565    /// SymbolId直接指定フィルタ
566    SymbolId(String),
567    /// Body パターンマッチフィルタ
568    BodyMatch(ryo_pattern::BodyMatch),
569    /// Relations フィルタ (any/all/none)
570    Relations(ryo_pattern::Relations),
571}
572
573#[cfg(test)]
574mod tests {
575    use super::*;
576    use crate::parser::QueryParser;
577
578    #[test]
579    fn test_convert_simple_function_query() {
580        let yaml = r#"
581kind: Function
582match:
583  name: "process"
584"#;
585        let query = QueryParser::from_yaml(yaml).unwrap();
586        let result = QueryConverter::to_discovery_query(&query).unwrap();
587
588        assert!(result.discovery_query.is_some());
589        assert!(result.composite.is_none());
590    }
591
592    #[test]
593    fn test_convert_glob_pattern() {
594        let yaml = r#"
595kind: Struct
596match:
597  name: { glob: "*Config" }
598"#;
599        let query = QueryParser::from_yaml(yaml).unwrap();
600        let result = QueryConverter::to_discovery_query(&query).unwrap();
601
602        let dq = result.discovery_query.unwrap();
603        assert!(dq.pattern.matches("AppConfig"));
604        assert!(dq.pattern.matches("Config"));
605        assert!(!dq.pattern.matches("ConfigManager"));
606    }
607
608    #[test]
609    fn test_convert_contains_to_glob() {
610        let yaml = r#"
611kind: Function
612match:
613  name: { contains: "process" }
614"#;
615        let query = QueryParser::from_yaml(yaml).unwrap();
616        let result = QueryConverter::to_discovery_query(&query).unwrap();
617
618        let dq = result.discovery_query.unwrap();
619        // contains: "process" → glob: "*process*"
620        assert!(dq.pattern.matches("process"));
621        assert!(dq.pattern.matches("process_event"));
622        assert!(dq.pattern.matches("do_process"));
623    }
624
625    #[test]
626    fn test_convert_with_post_filters() {
627        let yaml = r#"
628kind: Function
629match:
630  name: "handler"
631  is_async: true
632  vis: Public
633"#;
634        let query = QueryParser::from_yaml(yaml).unwrap();
635        let result = QueryConverter::to_discovery_query(&query).unwrap();
636
637        // is_async, visはpost_filtersに入る
638        assert!(result
639            .post_filters
640            .iter()
641            .any(|f| matches!(f, PostFilter::IsAsync(true))));
642        assert!(result
643            .post_filters
644            .iter()
645            .any(|f| matches!(f, PostFilter::Visibility(_))));
646        assert!(!result.unsupported.is_empty());
647    }
648
649    #[test]
650    fn test_convert_or_query() {
651        let yaml = r#"
652kind: Or
653queries:
654  - kind: Struct
655    match:
656      name: { contains: "Error" }
657  - kind: Enum
658    match:
659      name: { contains: "Error" }
660"#;
661        let query = QueryParser::from_yaml(yaml).unwrap();
662        let result = QueryConverter::to_discovery_query(&query).unwrap();
663
664        assert!(result.discovery_query.is_none());
665        assert!(result.composite.is_some());
666
667        let composite = result.composite.unwrap();
668        assert_eq!(composite.op, CompositeOp::Or);
669        assert_eq!(composite.queries.len(), 2);
670    }
671
672    #[test]
673    fn test_convert_with_scope() {
674        let yaml = r#"
675kind: Function
676match:
677  name: "*"
678scope:
679  module: "handlers"
680  path: "src/**"
681"#;
682        let query = QueryParser::from_yaml(yaml).unwrap();
683        let result = QueryConverter::to_discovery_query(&query).unwrap();
684
685        let dq = result.discovery_query.unwrap();
686        assert_eq!(dq.in_module, Some("handlers".to_string()));
687
688        // path は後処理
689        assert!(result
690            .post_filters
691            .iter()
692            .any(|f| matches!(f, PostFilter::PathInclude(_))));
693    }
694
695    #[test]
696    fn test_convert_with_inner() {
697        let yaml = r#"
698kind: Function
699match:
700  name: { starts_with: "process_" }
701inner:
702  - kind: ReturnType
703    match:
704      name: "Result"
705"#;
706        let query = QueryParser::from_yaml(yaml).unwrap();
707        let result = QueryConverter::to_discovery_query(&query).unwrap();
708
709        // inner条件はPostFilterに変換される
710        assert!(result
711            .post_filters
712            .iter()
713            .any(|f| matches!(f, PostFilter::ReturnType(_))));
714    }
715
716    #[test]
717    fn test_convert_with_multiple_inner() {
718        let yaml = r#"
719kind: Function
720match:
721  name: "*"
722inner:
723  - kind: ReturnType
724    match:
725      name: { contains: "Result" }
726  - kind: Parameter
727    match:
728      name: { contains: "Config" }
729"#;
730        let query = QueryParser::from_yaml(yaml).unwrap();
731        let result = QueryConverter::to_discovery_query(&query).unwrap();
732
733        // inner条件はPostFilterに変換される
734        assert!(result
735            .post_filters
736            .iter()
737            .any(|f| matches!(f, PostFilter::ReturnType(_))));
738        assert!(result
739            .post_filters
740            .iter()
741            .any(|f| matches!(f, PostFilter::ParamType(_))));
742    }
743
744    #[test]
745    fn test_convert_field_inner() {
746        let yaml = r#"
747kind: Struct
748match:
749  name: "*"
750inner:
751  - kind: Field
752    match:
753      name: "String"
754"#;
755        let query = QueryParser::from_yaml(yaml).unwrap();
756        let result = QueryConverter::to_discovery_query(&query).unwrap();
757
758        // inner条件はPostFilterに変換される
759        assert!(result
760            .post_filters
761            .iter()
762            .any(|f| matches!(f, PostFilter::FieldType(_))));
763    }
764
765    #[test]
766    fn test_convert_kind_function_excludes_method() {
767        // BUG#2 regression: QueryKind::Function must map to SymbolKind::Function only
768        let kinds = QueryConverter::convert_kind(QueryKind::Function).unwrap();
769        assert_eq!(kinds, vec![SymbolKind::Function]);
770        assert!(!kinds.contains(&SymbolKind::Method));
771    }
772
773    #[test]
774    fn test_convert_kind_struct_single() {
775        let kinds = QueryConverter::convert_kind(QueryKind::Struct).unwrap();
776        assert_eq!(kinds, vec![SymbolKind::Struct]);
777    }
778
779    #[test]
780    fn test_convert_generics_bounds_to_type_filter() {
781        let yaml = r#"
782kind: Function
783match:
784  name: "*"
785  generics:
786    bounds: ["Clone"]
787"#;
788        let query = QueryParser::from_yaml(yaml).unwrap();
789        let result = QueryConverter::to_discovery_query(&query).unwrap();
790
791        // bounds は TypeFilter.has_bound に変換される
792        let dq = result.discovery_query.unwrap();
793        assert!(dq.type_filter.is_some());
794
795        let type_filter = dq.type_filter.unwrap();
796        assert!(type_filter.has_bound.is_some());
797        assert!(type_filter.has_bound.unwrap().matches("Clone"));
798    }
799
800    #[test]
801    fn test_convert_generics_params_to_post_filter() {
802        let yaml = r#"
803kind: Function
804match:
805  name: "*"
806  generics:
807    params: ["T", "E"]
808    lifetimes: ["'a"]
809"#;
810        let query = QueryParser::from_yaml(yaml).unwrap();
811        let result = QueryConverter::to_discovery_query(&query).unwrap();
812
813        // params/lifetimes は PostFilter::Generics に残る
814        assert!(result.post_filters.iter().any(|f| {
815            matches!(f, PostFilter::Generics(g) if g.params.is_some() && g.bounds.is_none())
816        }));
817    }
818
819    #[test]
820    fn test_convert_generics_mixed_bounds_and_params() {
821        let yaml = r#"
822kind: Struct
823match:
824  name: "*"
825  generics:
826    params: ["T"]
827    bounds: [{ glob: "*Send*" }]
828"#;
829        let query = QueryParser::from_yaml(yaml).unwrap();
830        let result = QueryConverter::to_discovery_query(&query).unwrap();
831
832        // bounds は TypeFilter へ
833        let dq = result.discovery_query.unwrap();
834        let type_filter = dq.type_filter.unwrap();
835        assert!(type_filter.has_bound.is_some());
836        assert!(type_filter.has_bound.unwrap().matches("MySendTrait"));
837
838        // params は PostFilter へ
839        assert!(result.post_filters.iter().any(|f| {
840            matches!(f, PostFilter::Generics(g) if g.params.is_some() && g.bounds.is_none())
841        }));
842    }
843
844    #[test]
845    fn test_convert_symbol_id_filter() {
846        let json = r#"{"kind":"Function","match":{"symbol_id":"2421v1"}}"#;
847        let query = QueryParser::from_json(json).unwrap();
848        let result = QueryConverter::to_discovery_query(&query).unwrap();
849
850        assert!(
851            result
852                .post_filters
853                .iter()
854                .any(|f| matches!(f, PostFilter::SymbolId(ref s) if s == "2421v1")),
855            "symbol_id must be converted to PostFilter::SymbolId"
856        );
857    }
858
859    #[test]
860    fn test_convert_symbol_id_with_prefix() {
861        let json = r#"{"kind":"Any","match":{"symbol_id":"SymbolId(165v1)"}}"#;
862        let query = QueryParser::from_json(json).unwrap();
863        let result = QueryConverter::to_discovery_query(&query).unwrap();
864
865        assert!(
866            result
867                .post_filters
868                .iter()
869                .any(|f| matches!(f, PostFilter::SymbolId(ref s) if s == "SymbolId(165v1)")),
870            "SymbolId(xxx) format must also be preserved in PostFilter"
871        );
872    }
873
874    #[test]
875    fn test_convert_body_contains_filter() {
876        let json = r#"{
877            "kind": "Function",
878            "body": {
879                "contains": [
880                    {"node": "MethodCall"}
881                ]
882            }
883        }"#;
884        let query = QueryParser::from_json(json).unwrap();
885        let result = QueryConverter::to_discovery_query(&query).unwrap();
886
887        assert!(
888            result
889                .post_filters
890                .iter()
891                .any(|f| matches!(f, PostFilter::BodyMatch(_))),
892            "body.contains must be converted to PostFilter::BodyMatch"
893        );
894    }
895
896    #[test]
897    fn test_convert_body_not_contains_filter() {
898        let json = r#"{
899            "kind": "Function",
900            "body": {
901                "not_contains": [
902                    {"node": "MethodCall", "children": {"method": {"name": "unwrap"}}}
903                ]
904            }
905        }"#;
906        let query = QueryParser::from_json(json).unwrap();
907        let result = QueryConverter::to_discovery_query(&query).unwrap();
908
909        assert!(
910            result
911                .post_filters
912                .iter()
913                .any(|f| matches!(f, PostFilter::BodyMatch(bm) if bm.not_contains.is_some())),
914            "body.not_contains must be preserved in PostFilter::BodyMatch"
915        );
916    }
917
918    #[test]
919    fn test_convert_relations_any_filter() {
920        let json = r#"{
921            "kind": "Function",
922            "relations": {
923                "any": [
924                    {"kind": "Calls", "target": {"kind": "Function", "match": {"name": "serve"}}}
925                ]
926            }
927        }"#;
928        let query = QueryParser::from_json(json).unwrap();
929        let result = QueryConverter::to_discovery_query(&query).unwrap();
930
931        assert!(
932            result
933                .post_filters
934                .iter()
935                .any(|f| matches!(f, PostFilter::Relations(r) if r.any.is_some())),
936            "relations.any must be converted to PostFilter::Relations"
937        );
938    }
939
940    #[test]
941    fn test_convert_relations_none_filter() {
942        let json = r#"{
943            "kind": "Trait",
944            "relations": {
945                "none": [
946                    {"kind": "ImplementedBy", "target": {"kind": "Struct", "match": {"name": "Router"}}}
947                ]
948            }
949        }"#;
950        let query = QueryParser::from_json(json).unwrap();
951        let result = QueryConverter::to_discovery_query(&query).unwrap();
952
953        assert!(
954            result
955                .post_filters
956                .iter()
957                .any(|f| matches!(f, PostFilter::Relations(r) if r.none.is_some())),
958            "relations.none must be converted to PostFilter::Relations"
959        );
960    }
961
962    #[test]
963    fn test_convert_empty_relations_skipped() {
964        let json = r#"{"kind": "Function", "relations": {}}"#;
965        let query = QueryParser::from_json(json).unwrap();
966        let result = QueryConverter::to_discovery_query(&query).unwrap();
967
968        assert!(
969            !result
970                .post_filters
971                .iter()
972                .any(|f| matches!(f, PostFilter::Relations(_))),
973            "empty relations must not produce PostFilter"
974        );
975    }
976}