Skip to main content

ryo_query_language/
filter.rs

1//! Post-processing filters for query results.
2//!
3//! DiscoveryQueryでは表現できない条件を後処理で適用する。
4
5use glob::Pattern as GlobPattern;
6use ryo_analysis::{AnalysisContext, DiscoveredSymbol, Pattern, Visibility as AnalysisVisibility};
7
8use crate::converter::PostFilter;
9use crate::schema::{GenericsMatch, ReceiverKind, Visibility as RyoQlVisibility};
10
11/// 後処理フィルタプロセッサ
12pub struct PostFilterProcessor<'a> {
13    ctx: &'a AnalysisContext,
14}
15
16impl<'a> PostFilterProcessor<'a> {
17    /// 新しいプロセッサを作成
18    pub fn new(ctx: &'a AnalysisContext) -> Self {
19        Self { ctx }
20    }
21
22    /// 後処理フィルタを適用
23    ///
24    /// DetailStoreを使用してシンボル詳細を取得し、各フィルタを適用する。
25    pub fn apply(
26        &self,
27        results: Vec<DiscoveredSymbol>,
28        filters: &[PostFilter],
29    ) -> Vec<DiscoveredSymbol> {
30        if filters.is_empty() {
31            return results;
32        }
33
34        let mut filtered = results;
35
36        for filter in filters {
37            match filter {
38                PostFilter::IsAsync(expected) => {
39                    filtered = self.filter_by_async(filtered, *expected);
40                }
41                PostFilter::IsUnsafe(expected) => {
42                    filtered = self.filter_by_unsafe(filtered, *expected);
43                }
44                PostFilter::Visibility(vis) => {
45                    filtered = self.filter_by_visibility(filtered, vis);
46                }
47                PostFilter::Receiver(receiver) => {
48                    filtered = self.filter_by_receiver(filtered, receiver);
49                }
50                PostFilter::Attributes(attrs) => {
51                    filtered = self.filter_by_attributes(filtered, attrs);
52                }
53                PostFilter::Generics(generics) => {
54                    filtered = self.filter_by_generics(filtered, generics);
55                }
56                PostFilter::PathInclude(pattern) => {
57                    filtered = self.filter_by_path_include(filtered, pattern);
58                }
59                PostFilter::PathExclude(pattern) => {
60                    filtered = self.filter_by_path_exclude(filtered, pattern);
61                }
62                PostFilter::PatternSearch(_) => {
63                    // Phase 2で実装(Pattern Registry連携後)
64                }
65                PostFilter::OnEmpty => {
66                    // Executor側で処理(try_recovery)
67                }
68                PostFilter::ReturnType(pattern) => {
69                    filtered = self.filter_by_return_type(filtered, pattern);
70                }
71                PostFilter::ParamType(pattern) => {
72                    filtered = self.filter_by_param_type(filtered, pattern);
73                }
74                PostFilter::FieldType(pattern) => {
75                    filtered = self.filter_by_field_type(filtered, pattern);
76                }
77                PostFilter::Parent(pattern) => {
78                    filtered = self.filter_by_parent(filtered, pattern);
79                }
80                PostFilter::SymbolId(ref sid_str) => {
81                    filtered = self.filter_by_symbol_id(filtered, sid_str);
82                }
83                PostFilter::BodyMatch(ref body_match) => {
84                    filtered = self.filter_by_body(filtered, body_match);
85                }
86                PostFilter::Relations(ref relations) => {
87                    filtered = self.filter_by_relations(filtered, relations);
88                }
89            }
90        }
91
92        filtered
93    }
94
95    /// 戻り値型でフィルタ (DetailStore使用)
96    fn filter_by_return_type(
97        &self,
98        results: Vec<DiscoveredSymbol>,
99        pattern: &str,
100    ) -> Vec<DiscoveredSymbol> {
101        results
102            .into_iter()
103            .filter(|symbol| {
104                if let Some(detail) = self.ctx.detail_store.function(symbol.id) {
105                    if let Some(ref ret_type) = detail.return_type {
106                        return matches_pattern(ret_type, pattern);
107                    }
108                }
109                false
110            })
111            .collect()
112    }
113
114    /// パラメータ型でフィルタ (DetailStore使用)
115    fn filter_by_param_type(
116        &self,
117        results: Vec<DiscoveredSymbol>,
118        pattern: &str,
119    ) -> Vec<DiscoveredSymbol> {
120        results
121            .into_iter()
122            .filter(|symbol| {
123                if let Some(detail) = self.ctx.detail_store.function(symbol.id) {
124                    for param in &detail.params {
125                        if matches_pattern(&param.ty, pattern) {
126                            return true;
127                        }
128                    }
129                }
130                false
131            })
132            .collect()
133    }
134
135    /// フィールド型でフィルタ (DetailStore使用)
136    fn filter_by_field_type(
137        &self,
138        results: Vec<DiscoveredSymbol>,
139        pattern: &str,
140    ) -> Vec<DiscoveredSymbol> {
141        results
142            .into_iter()
143            .filter(|symbol| {
144                // Struct fields
145                if let Some(detail) = self.ctx.detail_store.struct_(symbol.id) {
146                    for field in &detail.fields {
147                        if matches_pattern(&field.ty, pattern) {
148                            return true;
149                        }
150                    }
151                }
152                // Enum variant fields
153                if let Some(detail) = self.ctx.detail_store.enum_(symbol.id) {
154                    for variant in &detail.variants {
155                        for field in &variant.fields {
156                            if matches_pattern(&field.ty, pattern) {
157                                return true;
158                            }
159                        }
160                    }
161                }
162                false
163            })
164            .collect()
165    }
166
167    /// async関数でフィルタ
168    fn filter_by_async(
169        &self,
170        results: Vec<DiscoveredSymbol>,
171        expected: bool,
172    ) -> Vec<DiscoveredSymbol> {
173        results
174            .into_iter()
175            .filter(|symbol| {
176                if let Some(detail) = self.ctx.detail_store.function(symbol.id) {
177                    detail.is_async == expected
178                } else {
179                    // 関数でない場合はasync=falseとみなす
180                    !expected
181                }
182            })
183            .collect()
184    }
185
186    /// unsafe関数でフィルタ
187    fn filter_by_unsafe(
188        &self,
189        results: Vec<DiscoveredSymbol>,
190        expected: bool,
191    ) -> Vec<DiscoveredSymbol> {
192        results
193            .into_iter()
194            .filter(|symbol| {
195                // 関数の場合
196                if let Some(detail) = self.ctx.detail_store.function(symbol.id) {
197                    return detail.is_unsafe == expected;
198                }
199                // トレイトの場合
200                if let Some(detail) = self.ctx.detail_store.trait_(symbol.id) {
201                    return detail.is_unsafe == expected;
202                }
203                // impl の場合
204                if let Some(detail) = self.ctx.detail_store.impl_(symbol.id) {
205                    return detail.is_unsafe == expected;
206                }
207                // その他はunsafe=falseとみなす
208                !expected
209            })
210            .collect()
211    }
212
213    /// 可視性でフィルタ
214    fn filter_by_visibility(
215        &self,
216        results: Vec<DiscoveredSymbol>,
217        expected: &RyoQlVisibility,
218    ) -> Vec<DiscoveredSymbol> {
219        results
220            .into_iter()
221            .filter(|symbol| {
222                if let Some(vis) = &symbol.visibility {
223                    matches_visibility(vis, expected)
224                } else if let Some(vis) = self.ctx.registry.visibility(symbol.id) {
225                    matches_visibility(vis, expected)
226                } else {
227                    // 可視性情報がない場合はPrivateとみなす
228                    matches!(expected, RyoQlVisibility::Private)
229                }
230            })
231            .collect()
232    }
233
234    /// レシーバー種別でフィルタ
235    fn filter_by_receiver(
236        &self,
237        results: Vec<DiscoveredSymbol>,
238        expected: &ReceiverKind,
239    ) -> Vec<DiscoveredSymbol> {
240        results
241            .into_iter()
242            .filter(|symbol| {
243                if let Some(detail) = self.ctx.detail_store.function(symbol.id) {
244                    let actual = get_receiver_kind(&detail.params);
245                    actual == *expected
246                } else {
247                    // 関数でない場合はNone(関連関数扱い)
248                    *expected == ReceiverKind::None
249                }
250            })
251            .collect()
252    }
253
254    /// アトリビュートでフィルタ
255    ///
256    /// シンボルに指定された全てのアトリビュートが含まれている場合にマッチ。
257    /// 例: `attributes: ["deprecated"]` → `#[deprecated]` を持つシンボルにマッチ
258    fn filter_by_attributes(
259        &self,
260        results: Vec<DiscoveredSymbol>,
261        expected_attrs: &[String],
262    ) -> Vec<DiscoveredSymbol> {
263        if expected_attrs.is_empty() {
264            return results;
265        }
266
267        results
268            .into_iter()
269            .filter(|symbol| {
270                let actual_attrs = self.get_attrs(symbol.id);
271                // 全ての期待するアトリビュートが存在するか確認
272                expected_attrs.iter().all(|expected| {
273                    actual_attrs.iter().any(|actual| {
274                        // 完全一致またはパターンマッチ
275                        actual == expected || matches_pattern(actual, expected)
276                    })
277                })
278            })
279            .collect()
280    }
281
282    /// シンボルのアトリビュート情報を取得
283    fn get_attrs(&self, id: ryo_analysis::SymbolId) -> Vec<String> {
284        if let Some(detail) = self.ctx.detail_store.function(id) {
285            return detail.attrs.clone();
286        }
287        if let Some(detail) = self.ctx.detail_store.struct_(id) {
288            return detail.attrs.clone();
289        }
290        if let Some(detail) = self.ctx.detail_store.enum_(id) {
291            return detail.attrs.clone();
292        }
293        if let Some(detail) = self.ctx.detail_store.trait_(id) {
294            return detail.attrs.clone();
295        }
296        if let Some(detail) = self.ctx.detail_store.impl_(id) {
297            return detail.attrs.clone();
298        }
299        Vec::new()
300    }
301
302    /// ジェネリクス条件でフィルタ
303    fn filter_by_generics(
304        &self,
305        results: Vec<DiscoveredSymbol>,
306        expected: &GenericsMatch,
307    ) -> Vec<DiscoveredSymbol> {
308        results
309            .into_iter()
310            .filter(|symbol| {
311                let generics = self.get_generics(symbol.id);
312                matches_generics(&generics, expected)
313            })
314            .collect()
315    }
316
317    /// シンボルのジェネリクス情報を取得
318    fn get_generics(&self, id: ryo_analysis::SymbolId) -> Option<ryo_analysis::GenericInfo> {
319        if let Some(detail) = self.ctx.detail_store.function(id) {
320            return Some(detail.generics.clone());
321        }
322        if let Some(detail) = self.ctx.detail_store.struct_(id) {
323            return Some(detail.generics.clone());
324        }
325        if let Some(detail) = self.ctx.detail_store.enum_(id) {
326            return Some(detail.generics.clone());
327        }
328        if let Some(detail) = self.ctx.detail_store.trait_(id) {
329            return Some(detail.generics.clone());
330        }
331        if let Some(detail) = self.ctx.detail_store.impl_(id) {
332            return Some(detail.generics.clone());
333        }
334        None
335    }
336
337    /// パスincludeでフィルタ
338    fn filter_by_path_include(
339        &self,
340        results: Vec<DiscoveredSymbol>,
341        pattern: &str,
342    ) -> Vec<DiscoveredSymbol> {
343        let Ok(glob) = GlobPattern::new(pattern) else {
344            return results; // パターン不正の場合はスキップ
345        };
346
347        results
348            .into_iter()
349            .filter(|symbol| {
350                if let Some(ref span) = symbol.span {
351                    glob.matches(span.file.as_relative().to_string_lossy().as_ref())
352                } else {
353                    false // spanがない場合は除外
354                }
355            })
356            .collect()
357    }
358
359    /// パスexcludeでフィルタ
360    fn filter_by_path_exclude(
361        &self,
362        results: Vec<DiscoveredSymbol>,
363        pattern: &str,
364    ) -> Vec<DiscoveredSymbol> {
365        let Ok(glob) = GlobPattern::new(pattern) else {
366            return results; // パターン不正の場合はスキップ
367        };
368
369        results
370            .into_iter()
371            .filter(|symbol| {
372                if let Some(ref span) = symbol.span {
373                    !glob.matches(span.file.as_relative().to_string_lossy().as_ref())
374                } else {
375                    true // spanがない場合は保持
376                }
377            })
378            .collect()
379    }
380
381    /// 親シンボルでフィルタ(Variant, Field, Method用)
382    ///
383    /// シンボルパスから親のパスを抽出し、パターンとマッチング。
384    /// 例: `ryo_analysis::Filter::Include` → 親は `Filter`
385    fn filter_by_parent(
386        &self,
387        results: Vec<DiscoveredSymbol>,
388        pattern: &Pattern,
389    ) -> Vec<DiscoveredSymbol> {
390        results
391            .into_iter()
392            .filter(|symbol| {
393                // シンボルパスから親の名前を取得
394                let path_str = symbol.path.to_string();
395                let parts: Vec<&str> = path_str.split("::").collect();
396
397                // 親は最後から2番目のセグメント
398                if parts.len() >= 2 {
399                    let parent_name = parts[parts.len() - 2];
400                    pattern.matches(parent_name)
401                } else {
402                    // 親がない(トップレベル)場合は除外
403                    false
404                }
405            })
406            .collect()
407    }
408
409    /// SymbolIdでフィルタ(直接lookup)
410    ///
411    /// "165v1" または "SymbolId(165v1)" 形式の文字列を解析し、
412    /// 一致するシンボルのみを返す。
413    fn filter_by_symbol_id(
414        &self,
415        results: Vec<DiscoveredSymbol>,
416        sid_str: &str,
417    ) -> Vec<DiscoveredSymbol> {
418        let Some(target_id) = ryo_analysis::SymbolId::parse(sid_str) else {
419            // パースできない場合は結果を空にする(無効なIDで全件返すのは誤り)
420            return vec![];
421        };
422
423        results
424            .into_iter()
425            .filter(|symbol| symbol.id == target_id)
426            .collect()
427    }
428
429    /// Body パターンマッチフィルタ
430    ///
431    /// ASTRegistry から関数bodyを取得し、BodyScanner でパターンをチェック。
432    /// - `contains`: 各パターンに1つ以上マッチが必要
433    /// - `not_contains`: 各パターンにマッチが0件であること
434    /// - `all_of`: 全パターンに1つ以上マッチが必要
435    fn filter_by_body(
436        &self,
437        results: Vec<DiscoveredSymbol>,
438        body_match: &ryo_pattern::BodyMatch,
439    ) -> Vec<DiscoveredSymbol> {
440        use ryo_pattern::BodyScanner;
441        use ryo_source::pure::PureItem;
442
443        results
444            .into_iter()
445            .filter(|symbol| {
446                // ASTRegistry から PureFn を取得
447                let Some(PureItem::Fn(fn_item)) = self.ctx.ast_registry.get(symbol.id) else {
448                    // 関数以外はbodyがないため除外
449                    return false;
450                };
451
452                // contains: 各パターンに1つ以上マッチが必要
453                if let Some(ref patterns) = body_match.contains {
454                    for pattern in patterns {
455                        let scanner = BodyScanner::new(pattern);
456                        if scanner.scan_fn(fn_item).is_empty() {
457                            return false;
458                        }
459                    }
460                }
461
462                // not_contains: 各パターンにマッチが0件であること
463                if let Some(ref patterns) = body_match.not_contains {
464                    for pattern in patterns {
465                        let scanner = BodyScanner::new(pattern);
466                        if !scanner.scan_fn(fn_item).is_empty() {
467                            return false;
468                        }
469                    }
470                }
471
472                // all_of: 全パターンに1つ以上マッチが必要(containsと同じロジック)
473                if let Some(ref patterns) = body_match.all_of {
474                    for pattern in patterns {
475                        let scanner = BodyScanner::new(pattern);
476                        if scanner.scan_fn(fn_item).is_empty() {
477                            return false;
478                        }
479                    }
480                }
481
482                true
483            })
484            .collect()
485    }
486
487    /// Relations フィルタ (any/all/none)
488    ///
489    /// グラフAPIを使用して関係を照会し、ターゲット条件にマッチするか確認。
490    /// - `any`: 少なくとも1つの条件がマッチ
491    /// - `all`: 全条件がマッチ
492    /// - `none`: どの条件もマッチしない
493    fn filter_by_relations(
494        &self,
495        results: Vec<DiscoveredSymbol>,
496        relations: &ryo_pattern::Relations,
497    ) -> Vec<DiscoveredSymbol> {
498        results
499            .into_iter()
500            .filter(|symbol| {
501                // any: 少なくとも1つの条件がマッチ
502                if let Some(ref conditions) = relations.any {
503                    if !conditions
504                        .iter()
505                        .any(|rel| self.check_relation(symbol.id, rel))
506                    {
507                        return false;
508                    }
509                }
510
511                // all: 全条件がマッチ
512                if let Some(ref conditions) = relations.all {
513                    if !conditions
514                        .iter()
515                        .all(|rel| self.check_relation(symbol.id, rel))
516                    {
517                        return false;
518                    }
519                }
520
521                // none: どの条件もマッチしない
522                if let Some(ref conditions) = relations.none {
523                    if conditions
524                        .iter()
525                        .any(|rel| self.check_relation(symbol.id, rel))
526                    {
527                        return false;
528                    }
529                }
530
531                true
532            })
533            .collect()
534    }
535
536    /// 単一の関係条件をチェック
537    fn check_relation(
538        &self,
539        source_id: ryo_analysis::SymbolId,
540        relation: &ryo_pattern::Relation,
541    ) -> bool {
542        use ryo_pattern::RelationKind;
543
544        let related_ids: Vec<ryo_analysis::SymbolId> = match relation.kind {
545            RelationKind::Calls => self.ctx.code_graph.callees_of(source_id).collect(),
546            RelationKind::CalledBy => self.ctx.code_graph.callers_of(source_id).collect(),
547            RelationKind::TypeReferences => {
548                self.ctx.typeflow_graph.types_used_by(source_id).collect()
549            }
550            RelationKind::TypeReferencedBy => {
551                self.ctx.typeflow_graph.type_users(source_id).collect()
552            }
553            RelationKind::Implements => {
554                // outgoing Implements edges (Impl → Trait)
555                let mut result: Vec<ryo_analysis::SymbolId> = self
556                    .ctx
557                    .code_graph
558                    .outgoing_edges(source_id)
559                    .filter(|e| e.kind == ryo_analysis::CodeEdgeV2::Implements)
560                    .map(|e| e.to)
561                    .collect();
562
563                // For Struct/Enum: find Impl blocks that target this type,
564                // then follow their Implements edges to Traits
565                if result.is_empty() {
566                    let source_kind = self.ctx.registry.kind(source_id);
567                    if matches!(
568                        source_kind,
569                        Some(ryo_analysis::SymbolKind::Struct)
570                            | Some(ryo_analysis::SymbolKind::Enum)
571                    ) {
572                        if let Some(source_path) = self.ctx.registry.resolve(source_id) {
573                            let source_name = source_path.name();
574                            // Find Impl blocks whose self_ty matches this type
575                            for impl_id in self
576                                .ctx
577                                .registry
578                                .iter_by_kind(ryo_analysis::SymbolKind::Impl)
579                            {
580                                if let Some(impl_path) = self.ctx.registry.resolve(impl_id) {
581                                    if let Some(last_seg) = impl_path.segment_refs().last() {
582                                        if let Some(self_ty) = last_seg.impl_self_ty() {
583                                            // Strip generics: "Writer < '_ >" → "Writer"
584                                            let base =
585                                                self_ty.split('<').next().unwrap_or(self_ty).trim();
586                                            if base == source_name {
587                                                // Follow this Impl's Implements edges
588                                                for e in self.ctx.code_graph.outgoing_edges(impl_id)
589                                                {
590                                                    if e.kind
591                                                        == ryo_analysis::CodeEdgeV2::Implements
592                                                    {
593                                                        result.push(e.to);
594                                                    }
595                                                }
596                                            }
597                                        }
598                                    }
599                                }
600                            }
601                        }
602                    }
603                }
604
605                result
606            }
607            RelationKind::ImplementedBy => {
608                // Impl blocks that implement this Trait
609                let impl_ids: Vec<ryo_analysis::SymbolId> =
610                    self.ctx.code_graph.implementors_of(source_id).collect();
611
612                // Also resolve Impl → target Struct/Enum for broader matching
613                let mut result = impl_ids.clone();
614                for &impl_id in &impl_ids {
615                    if let Some(impl_path) = self.ctx.registry.resolve(impl_id) {
616                        if let Some(last_seg) = impl_path.segment_refs().last() {
617                            if let Some(self_ty) = last_seg.impl_self_ty() {
618                                let base = self_ty.split('<').next().unwrap_or(self_ty).trim();
619                                // Find the Struct/Enum with this name
620                                if let Some(struct_id) = self.ctx.registry.lookup_by_name(base) {
621                                    let kind = self.ctx.registry.kind(struct_id);
622                                    if matches!(
623                                        kind,
624                                        Some(ryo_analysis::SymbolKind::Struct)
625                                            | Some(ryo_analysis::SymbolKind::Enum)
626                                    ) {
627                                        result.push(struct_id);
628                                    }
629                                }
630                            }
631                        }
632                    }
633                }
634
635                result
636            }
637            RelationKind::Contains => self.ctx.code_graph.children_of(source_id).collect(),
638            RelationKind::ContainedBy => self
639                .ctx
640                .code_graph
641                .parent_of(source_id)
642                .into_iter()
643                .collect(),
644        };
645
646        // ターゲット条件にマッチする関連シンボルが存在するか
647        related_ids
648            .iter()
649            .any(|&related_id| self.matches_target(related_id, &relation.target))
650    }
651
652    /// ターゲット条件にマッチするかチェック
653    fn matches_target(
654        &self,
655        id: ryo_analysis::SymbolId,
656        target: &ryo_pattern::RelationTarget,
657    ) -> bool {
658        // kind フィルタ
659        if let Some(ref target_kind) = target.kind {
660            let actual_kind = self
661                .ctx
662                .registry
663                .kind(id)
664                .unwrap_or(ryo_analysis::SymbolKind::Other);
665            if !matches_target_kind(&actual_kind, target_kind) {
666                return false;
667            }
668        }
669
670        // name/pattern マッチ
671        if let Some(ref target_match) = target.r#match {
672            let Some(path) = self.ctx.registry.resolve(id) else {
673                return false;
674            };
675            let name = path.name();
676
677            if let Some(ref exact_name) = target_match.name {
678                if name != exact_name {
679                    return false;
680                }
681            }
682
683            if let Some(ref pattern) = target_match.pattern {
684                if !matches_pattern(name, pattern) {
685                    return false;
686                }
687            }
688
689            if let Some(ref regex_str) = target_match.regex {
690                if let Ok(re) = regex::Regex::new(regex_str) {
691                    if !re.is_match(name) {
692                        return false;
693                    }
694                }
695            }
696        }
697
698        true
699    }
700}
701
702// =============================================================================
703// Helper Functions
704// =============================================================================
705
706/// TargetKind → SymbolKind マッチング
707fn matches_target_kind(
708    actual: &ryo_analysis::SymbolKind,
709    expected: &ryo_pattern::TargetKind,
710) -> bool {
711    use ryo_analysis::SymbolKind;
712    use ryo_pattern::TargetKind;
713
714    matches!(
715        (actual, expected),
716        (SymbolKind::Function, TargetKind::Function)
717            | (SymbolKind::Method, TargetKind::Function) // Method も Function としてマッチ
718            | (SymbolKind::Struct, TargetKind::Struct)
719            | (SymbolKind::Enum, TargetKind::Enum)
720            | (SymbolKind::Trait, TargetKind::Trait)
721            | (SymbolKind::Impl, TargetKind::Impl)
722            | (SymbolKind::Mod, TargetKind::Mod)
723            | (SymbolKind::Const, TargetKind::Const)
724            | (SymbolKind::Static, TargetKind::Static)
725            | (SymbolKind::TypeAlias, TargetKind::TypeAlias)
726    )
727}
728
729/// パターンマッチング (glob-style)
730fn matches_pattern(text: &str, pattern: &str) -> bool {
731    // Handle special patterns
732    if pattern == "*" {
733        return true;
734    }
735
736    // Handle *contains* pattern
737    if pattern.starts_with('*') && pattern.ends_with('*') && pattern.len() > 2 {
738        let inner = &pattern[1..pattern.len() - 1];
739        return text.contains(inner);
740    }
741
742    // Handle prefix* pattern
743    if pattern.ends_with('*') && !pattern.starts_with('*') {
744        let prefix = &pattern[..pattern.len() - 1];
745        return text.starts_with(prefix);
746    }
747
748    // Handle *suffix pattern
749    if pattern.starts_with('*') && !pattern.ends_with('*') {
750        let suffix = &pattern[1..];
751        return text.ends_with(suffix);
752    }
753
754    // Handle regex: pattern
755    if let Some(regex_str) = pattern.strip_prefix("regex:") {
756        if let Ok(re) = regex::Regex::new(regex_str) {
757            return re.is_match(text);
758        }
759        return false;
760    }
761
762    // Exact match
763    text == pattern
764}
765
766/// AnalysisVisibility から RyoQL Visibility へのマッチング
767fn matches_visibility(actual: &AnalysisVisibility, expected: &RyoQlVisibility) -> bool {
768    match (actual, expected) {
769        (AnalysisVisibility::Public, RyoQlVisibility::Public) => true,
770        (AnalysisVisibility::Private, RyoQlVisibility::Private) => true,
771        (AnalysisVisibility::Crate, RyoQlVisibility::Crate) => true,
772        (AnalysisVisibility::Super, RyoQlVisibility::Super) => true,
773        (
774            AnalysisVisibility::Restricted(actual_path),
775            RyoQlVisibility::Restricted(expected_path),
776        ) => actual_path.to_string() == *expected_path,
777        _ => false,
778    }
779}
780
781/// パラメータリストからレシーバー種別を判定
782fn get_receiver_kind(params: &[ryo_analysis::ParamInfo]) -> ReceiverKind {
783    params.first().map_or(ReceiverKind::None, |first| {
784        if !first.is_self {
785            return ReceiverKind::None;
786        }
787        // self パラメータの型から判定
788        if first.ty.starts_with("&mut ") {
789            ReceiverKind::MutRef
790        } else if first.ty.starts_with('&') {
791            ReceiverKind::Ref
792        } else {
793            ReceiverKind::Owned
794        }
795    })
796}
797
798/// ジェネリクス条件のマッチング
799fn matches_generics(actual: &Option<ryo_analysis::GenericInfo>, expected: &GenericsMatch) -> bool {
800    let Some(actual) = actual else {
801        // ジェネリクスがない場合、条件もなければマッチ
802        return expected.params.is_none()
803            && expected.bounds.is_none()
804            && expected.lifetimes.is_none();
805    };
806
807    // params チェック
808    if let Some(ref expected_params) = expected.params {
809        for expected_param in expected_params {
810            if !actual.type_params.contains(expected_param) {
811                return false;
812            }
813        }
814    }
815
816    // lifetimes チェック
817    if let Some(ref expected_lifetimes) = expected.lifetimes {
818        for expected_lt in expected_lifetimes {
819            if !actual.lifetimes.contains(expected_lt) {
820                return false;
821            }
822        }
823    }
824
825    // bounds は TypeFilter で既に処理されているためここではスキップ
826    // (GenericsMatch.bounds は None になっている)
827
828    true
829}
830
831#[cfg(test)]
832mod tests {
833    use super::*;
834
835    #[test]
836    fn test_matches_pattern_exact() {
837        assert!(matches_pattern("Result", "Result"));
838        assert!(!matches_pattern("Result", "Option"));
839    }
840
841    #[test]
842    fn test_matches_pattern_wildcard() {
843        assert!(matches_pattern("anything", "*"));
844    }
845
846    #[test]
847    fn test_matches_pattern_contains() {
848        assert!(matches_pattern("MyResult", "*Result*"));
849        assert!(matches_pattern("ResultWrapper", "*Result*"));
850        assert!(!matches_pattern("Option", "*Result*"));
851    }
852
853    #[test]
854    fn test_matches_pattern_prefix() {
855        assert!(matches_pattern("ResultType", "Result*"));
856        assert!(!matches_pattern("MyResult", "Result*"));
857    }
858
859    #[test]
860    fn test_matches_pattern_suffix() {
861        assert!(matches_pattern("MyResult", "*Result"));
862        assert!(!matches_pattern("ResultType", "*Result"));
863    }
864}