Skip to main content

ryo_app/discover/
service.rs

1//! Discover Service - Application layer for pattern-based symbol discovery
2//!
3//! Provides high-level APIs for:
4//! - Symbol discovery with pattern matching
5//! - Relation graph queries (callers, callees, uses, used_by)
6//! - Cascade effect analysis for enum variants
7//! - Borrow check and lock analysis integration
8
9use super::response::{CascadeResult, DiscoverError};
10use crate::project::Project;
11use ryo_analysis::cascade::CascadeSpec;
12use ryo_analysis::{
13    AnalysisContext, DiscoveryEngine, DiscoveryQuery, DiscoveryResult, SymbolKind, SymbolPath,
14    SymbolRegistry,
15};
16use ryo_source::pure::{
17    PureBlock, PureExpr, PureImpl, PureImplItem, PureItem, PurePattern, PureStmt,
18};
19use std::path::Path;
20
21// ============================================================================
22// Discover Service
23// ============================================================================
24
25/// Service for pattern-based symbol discovery operations.
26pub struct DiscoverService {
27    ctx: AnalysisContext,
28}
29
30impl DiscoverService {
31    /// Create a new DiscoverService from a pre-built AnalysisContext.
32    ///
33    /// Use this when you have a cached or pre-built context (e.g., from CLI).
34    pub fn new(ctx: AnalysisContext) -> Self {
35        Self { ctx }
36    }
37
38    /// Create a new DiscoverService from a project.
39    ///
40    /// Uses Project's workspace_root for accurate crate name resolution.
41    pub fn from_project(project: &Project) -> Result<Self, DiscoverError> {
42        let ctx = AnalysisContext::from_workspace_root(project.workspace_root())
43            .map_err(|e| DiscoverError::Project(e.to_string()))?;
44        Ok(Self { ctx })
45    }
46
47    /// Create a new DiscoverService from a path.
48    pub fn from_path(path: &Path) -> Result<Self, DiscoverError> {
49        let ctx = AnalysisContext::from_workspace_root(path)
50            .map_err(|e| DiscoverError::Project(e.to_string()))?;
51        Ok(Self { ctx })
52    }
53
54    /// Get the underlying AnalysisContext.
55    pub fn context(&self) -> &AnalysisContext {
56        &self.ctx
57    }
58
59    /// Get the symbol registry.
60    pub fn registry(&self) -> &SymbolRegistry {
61        &self.ctx.registry
62    }
63
64    /// Execute a discovery query.
65    pub fn discover(&self, query: &DiscoveryQuery) -> DiscoveryResult {
66        let engine = DiscoveryEngine::new(&self.ctx.code_graph, &self.ctx.registry, None);
67        engine.execute(query)
68    }
69
70    /// Discover symbols matching a pattern.
71    pub fn find_symbols(&self, pattern: &str) -> DiscoveryResult {
72        let query = DiscoveryQuery::symbol(pattern);
73        self.discover(&query)
74    }
75
76    /// Discover symbols matching a pattern with kind filter.
77    pub fn find_symbols_by_kind(&self, pattern: &str, kind: SymbolKind) -> DiscoveryResult {
78        let query = DiscoveryQuery::symbol(pattern).kind(kind);
79        self.discover(&query)
80    }
81
82    /// Discover symbols with relation expansion.
83    pub fn find_with_relations(&self, pattern: &str, depth: usize) -> DiscoveryResult {
84        let query = DiscoveryQuery::symbol(pattern)
85            .with_relations()
86            .relation_depth(depth);
87        self.discover(&query)
88    }
89
90    /// Find cascade effects for an enum (match expressions that need new arms).
91    ///
92    /// Delegates to the freestanding [`find_cascade_effects`] function.
93    pub fn find_cascade_effects(
94        &self,
95        enum_pattern: &str,
96        variant_name: Option<&str>,
97        variant_type: Option<&str>,
98    ) -> CascadeResult {
99        find_cascade_effects(&self.ctx.files, enum_pattern, variant_name, variant_type)
100    }
101
102    /// Find cascade effects for removing a variant (match arms that should be deleted).
103    ///
104    /// Delegates to the freestanding [`find_remove_cascade_effects`] function.
105    pub fn find_remove_cascade_effects(
106        &self,
107        enum_pattern: &str,
108        variant_name: &str,
109    ) -> CascadeResult {
110        find_remove_cascade_effects(&self.ctx.files, enum_pattern, variant_name)
111    }
112}
113
114// ============================================================================
115// Cascade Analysis (freestanding)
116// ============================================================================
117
118/// Find cascade effects for an enum (match expressions that need new arms).
119///
120/// Scans `files` for match expressions on the target enum and returns
121/// `CascadeSpec`s that can be converted to `Intent`s.
122///
123/// This is a freestanding function — no `DiscoverService` required.
124/// Call directly with `&AnalysisContext.files`.
125///
126/// # Arguments
127/// - `variant_type`: Format is "unit", "tuple:T1,T2", or "struct:f1:T1,f2:T2"
128pub fn find_cascade_effects(
129    files: &ryo_analysis::ImHashMap<
130        ryo_symbol::WorkspaceFilePath,
131        std::sync::Arc<ryo_source::PureFile>,
132    >,
133    enum_pattern: &str,
134    variant_name: Option<&str>,
135    variant_type: Option<&str>,
136) -> CascadeResult {
137    let variant = variant_name.unwrap_or("NewVariant");
138    let vtype = variant_type.unwrap_or("unit");
139    let enum_name = enum_pattern.trim_start_matches('*').trim_end_matches('*');
140    let mut specs = Vec::new();
141
142    for (wfp, file) in files.iter() {
143        let module_path = wfp_to_symbol_path(wfp);
144
145        for func in file.functions() {
146            let function_name = func.name.clone();
147            find_match_in_block(
148                &func.body,
149                enum_name,
150                variant,
151                vtype,
152                &module_path,
153                &function_name,
154                &mut specs,
155            );
156        }
157
158        for item in &file.items {
159            if let PureItem::Impl(impl_block) = item {
160                let impl_path = impl_symbol_path(&module_path, impl_block);
161                for impl_item in &impl_block.items {
162                    if let PureImplItem::Fn(func) = impl_item {
163                        let function_name = func.name.clone();
164                        find_match_in_block(
165                            &func.body,
166                            enum_name,
167                            variant,
168                            vtype,
169                            &impl_path,
170                            &function_name,
171                            &mut specs,
172                        );
173                    }
174                }
175            }
176        }
177    }
178
179    // Dedup: at most one AddMatchArm spec per function.
180    // Multiple match expressions on the same enum in one function produce
181    // redundant specs; the mutation walker handles all matches internally.
182    {
183        let mut seen = Vec::new();
184        specs.retain(|spec| {
185            if let CascadeSpec::AddMatchArm {
186                target,
187                function_name,
188                ..
189            } = spec
190            {
191                let key = format!("{}::{}", target, function_name);
192                if seen.contains(&key) {
193                    false
194                } else {
195                    seen.push(key);
196                    true
197                }
198            } else {
199                true
200            }
201        });
202    }
203
204    CascadeResult {
205        symbol: enum_pattern.to_string(),
206        specs,
207    }
208}
209
210/// Find cascade effects for removing an enum variant (match arms to remove).
211///
212/// Scans `files` for match expressions on the target enum and returns
213/// `CascadeSpec::RemoveMatchArm` for arms whose pattern references the variant.
214pub fn find_remove_cascade_effects(
215    files: &ryo_analysis::ImHashMap<
216        ryo_symbol::WorkspaceFilePath,
217        std::sync::Arc<ryo_source::PureFile>,
218    >,
219    enum_pattern: &str,
220    variant_name: &str,
221) -> CascadeResult {
222    let enum_name = enum_pattern.trim_start_matches('*').trim_end_matches('*');
223    let mut specs = Vec::new();
224
225    for (wfp, file) in files.iter() {
226        let module_path = wfp_to_symbol_path(wfp);
227
228        for func in file.functions() {
229            let function_name = func.name.clone();
230            find_removable_arms_in_block(
231                &func.body,
232                enum_name,
233                variant_name,
234                &module_path,
235                &function_name,
236                &mut specs,
237            );
238        }
239
240        for item in &file.items {
241            if let PureItem::Impl(impl_block) = item {
242                let impl_path = impl_symbol_path(&module_path, impl_block);
243                for impl_item in &impl_block.items {
244                    if let PureImplItem::Fn(func) = impl_item {
245                        let function_name = func.name.clone();
246                        find_removable_arms_in_block(
247                            &func.body,
248                            enum_name,
249                            variant_name,
250                            &impl_path,
251                            &function_name,
252                            &mut specs,
253                        );
254                    }
255                }
256            }
257        }
258    }
259
260    CascadeResult {
261        symbol: enum_pattern.to_string(),
262        specs,
263    }
264}
265
266// ============================================================================
267// Internal Helpers for Cascade Analysis
268// ============================================================================
269
270/// Convert WorkspaceFilePath to SymbolPath (module path)
271fn wfp_to_symbol_path(wfp: &ryo_symbol::WorkspaceFilePath) -> SymbolPath {
272    // WorkspaceFilePath contains relative path from workspace root
273    // e.g., "crates/my_crate/src/foo/bar.rs"
274    let path_str = wfp.as_relative().to_string_lossy();
275    let crate_name = wfp.crate_name();
276
277    // Try to extract relative path after "src/"
278    let relative = if let Some(idx) = path_str.find("/src/") {
279        &path_str[idx + 5..]
280    } else if let Some(idx) = path_str.find("src/") {
281        &path_str[idx + 4..]
282    } else {
283        // Just use crate root
284        return SymbolPath::parse(crate_name.as_str()).unwrap_or_else(|_| {
285            SymbolPath::builder(crate_name.as_str())
286                .build()
287                .expect("valid crate name")
288        });
289    };
290
291    // Remove .rs extension and convert path separators to ::
292    let module_path = relative.trim_end_matches(".rs").replace('/', "::");
293
294    // Handle lib.rs and mod.rs specially (they represent the parent module)
295    let module_path = if module_path == "lib" || module_path.ends_with("::mod") {
296        let trimmed = module_path.trim_end_matches("::mod");
297        if trimmed.is_empty() || trimmed == "lib" {
298            crate_name.as_str().to_string()
299        } else {
300            format!("{}::{}", crate_name.as_str(), trimmed)
301        }
302    } else {
303        format!("{}::{}", crate_name.as_str(), module_path)
304    };
305
306    SymbolPath::parse(&module_path).unwrap_or_else(|_| {
307        SymbolPath::builder(crate_name.as_str())
308            .build()
309            .expect("valid crate name")
310    })
311}
312
313/// Construct the SymbolPath for an impl block's methods.
314///
315/// For inherent impls (`impl Foo`), the path is `module::Foo`.
316/// For trait impls (`impl Trait for Foo`), the path is `module::<impl Trait for Foo>`.
317///
318/// This matches the symbol registration format in the SymbolRegistry,
319/// enabling correct resolution when cascade specs target methods.
320fn impl_symbol_path(module_path: &SymbolPath, impl_block: &PureImpl) -> SymbolPath {
321    let segment = if let Some(trait_name) = &impl_block.trait_ {
322        // Trait impl: <impl Trait for Type>
323        format!("<impl {} for {}>", trait_name, impl_block.self_ty)
324    } else {
325        // Inherent impl: just the type name
326        impl_block.self_ty.clone()
327    };
328
329    module_path
330        .child(&segment)
331        .unwrap_or_else(|_| module_path.clone())
332}
333
334/// Generate match arm pattern based on variant type.
335///
336/// # Arguments
337/// - `variant_type`: "unit", "tuple:T1,T2", or "struct:f1:T1,f2:T2"
338fn generate_match_pattern(enum_name: &str, variant_name: &str, variant_type: &str) -> String {
339    if variant_type == "unit" || variant_type.is_empty() {
340        format!("{}::{}", enum_name, variant_name)
341    } else if let Some(types_part) = variant_type.strip_prefix("tuple:") {
342        // tuple:T1,T2 -> EnumName::Variant(_, _)
343        // Skip "tuple:"
344        let count = count_tuple_elements(types_part);
345        if count == 0 {
346            format!("{}::{}", enum_name, variant_name)
347        } else {
348            let wildcards = std::iter::repeat_n("_", count)
349                .collect::<Vec<_>>()
350                .join(", ");
351            format!("{}::{}({})", enum_name, variant_name, wildcards)
352        }
353    } else if let Some(fields_part) = variant_type.strip_prefix("struct:") {
354        // struct:f1:T1,f2:T2 -> EnumName::Variant { f1: _, f2: _ }
355        // Skip "struct:"
356        let field_patterns: Vec<&str> = fields_part
357            .split(',')
358            .filter_map(|f| {
359                let name = f.split(':').next()?;
360                if name.is_empty() {
361                    None
362                } else {
363                    Some(name)
364                }
365            })
366            .collect();
367        if field_patterns.is_empty() {
368            format!("{}::{} {{ .. }}", enum_name, variant_name)
369        } else {
370            let pattern = field_patterns
371                .iter()
372                .map(|f| format!("{}: _", f))
373                .collect::<Vec<_>>()
374                .join(", ");
375            format!("{}::{} {{ {} }}", enum_name, variant_name, pattern)
376        }
377    } else {
378        // Fallback to unit pattern
379        format!("{}::{}", enum_name, variant_name)
380    }
381}
382
383/// Count the number of tuple elements in a type string.
384///
385/// Handles nested generics by tracking angle bracket depth.
386/// Examples:
387/// - "i32,String" -> 2
388/// - "Option<usize>,Option<usize>" -> 2
389/// - "HashMap<K, V>,String" -> 2
390fn count_tuple_elements(types_str: &str) -> usize {
391    if types_str.is_empty() {
392        return 0;
393    }
394
395    let mut count = 1usize; // At least one element if not empty
396    let mut angle_depth = 0usize;
397
398    for c in types_str.chars() {
399        match c {
400            '<' => angle_depth += 1,
401            '>' => angle_depth = angle_depth.saturating_sub(1),
402            ',' if angle_depth == 0 => count += 1,
403            _ => {}
404        }
405    }
406
407    count
408}
409
410/// Find match expressions in a block
411fn find_match_in_block(
412    block: &PureBlock,
413    enum_name: &str,
414    variant_name: &str,
415    variant_type: &str,
416    module_path: &SymbolPath,
417    function_name: &str,
418    specs: &mut Vec<CascadeSpec>,
419) {
420    for stmt in &block.stmts {
421        match stmt {
422            PureStmt::Expr(expr) | PureStmt::Semi(expr) => {
423                find_match_in_expr(
424                    expr,
425                    enum_name,
426                    variant_name,
427                    variant_type,
428                    module_path,
429                    function_name,
430                    specs,
431                );
432            }
433            PureStmt::Local { init, .. } => {
434                if let Some(init_expr) = init {
435                    find_match_in_expr(
436                        init_expr,
437                        enum_name,
438                        variant_name,
439                        variant_type,
440                        module_path,
441                        function_name,
442                        specs,
443                    );
444                }
445            }
446            PureStmt::Item(_) => {}
447        }
448    }
449}
450
451/// Recursively find match expressions in an expression
452fn find_match_in_expr(
453    expr: &PureExpr,
454    enum_name: &str,
455    variant_name: &str,
456    variant_type: &str,
457    module_path: &SymbolPath,
458    function_name: &str,
459    specs: &mut Vec<CascadeSpec>,
460) {
461    match expr {
462        PureExpr::Match {
463            expr: scrutinee,
464            arms,
465        } => {
466            // Check if any arm pattern references the enum
467            let matches_enum = arms
468                .iter()
469                .any(|arm| pattern_contains_enum(&arm.pattern, enum_name));
470
471            if matches_enum {
472                let pattern = generate_match_pattern(enum_name, variant_name, variant_type);
473                specs.push(CascadeSpec::add_match_arm(
474                    module_path.clone(),
475                    function_name,
476                    enum_name,
477                    pattern,
478                    "todo!()",
479                ));
480            }
481
482            // Recurse into scrutinee
483            find_match_in_expr(
484                scrutinee,
485                enum_name,
486                variant_name,
487                variant_type,
488                module_path,
489                function_name,
490                specs,
491            );
492
493            // Recurse into arm bodies
494            for arm in arms {
495                find_match_in_expr(
496                    &arm.body,
497                    enum_name,
498                    variant_name,
499                    variant_type,
500                    module_path,
501                    function_name,
502                    specs,
503                );
504            }
505        }
506        PureExpr::Block { block, .. } => {
507            find_match_in_block(
508                block,
509                enum_name,
510                variant_name,
511                variant_type,
512                module_path,
513                function_name,
514                specs,
515            );
516        }
517        PureExpr::If {
518            cond,
519            then_branch,
520            else_branch,
521        } => {
522            find_match_in_expr(
523                cond,
524                enum_name,
525                variant_name,
526                variant_type,
527                module_path,
528                function_name,
529                specs,
530            );
531            find_match_in_block(
532                then_branch,
533                enum_name,
534                variant_name,
535                variant_type,
536                module_path,
537                function_name,
538                specs,
539            );
540            if let Some(else_expr) = else_branch {
541                find_match_in_expr(
542                    else_expr,
543                    enum_name,
544                    variant_name,
545                    variant_type,
546                    module_path,
547                    function_name,
548                    specs,
549                );
550            }
551        }
552        PureExpr::Loop { body: block, .. } | PureExpr::Unsafe(block) => {
553            find_match_in_block(
554                block,
555                enum_name,
556                variant_name,
557                variant_type,
558                module_path,
559                function_name,
560                specs,
561            );
562        }
563        PureExpr::Async { body, .. } => {
564            find_match_in_block(
565                body,
566                enum_name,
567                variant_name,
568                variant_type,
569                module_path,
570                function_name,
571                specs,
572            );
573        }
574        PureExpr::While { cond, body, .. } => {
575            find_match_in_expr(
576                cond,
577                enum_name,
578                variant_name,
579                variant_type,
580                module_path,
581                function_name,
582                specs,
583            );
584            find_match_in_block(
585                body,
586                enum_name,
587                variant_name,
588                variant_type,
589                module_path,
590                function_name,
591                specs,
592            );
593        }
594        PureExpr::For {
595            expr: iter, body, ..
596        } => {
597            find_match_in_expr(
598                iter,
599                enum_name,
600                variant_name,
601                variant_type,
602                module_path,
603                function_name,
604                specs,
605            );
606            find_match_in_block(
607                body,
608                enum_name,
609                variant_name,
610                variant_type,
611                module_path,
612                function_name,
613                specs,
614            );
615        }
616        PureExpr::Closure { body, .. } => {
617            find_match_in_expr(
618                body,
619                enum_name,
620                variant_name,
621                variant_type,
622                module_path,
623                function_name,
624                specs,
625            );
626        }
627        PureExpr::Call { func, args } => {
628            find_match_in_expr(
629                func,
630                enum_name,
631                variant_name,
632                variant_type,
633                module_path,
634                function_name,
635                specs,
636            );
637            for arg in args {
638                find_match_in_expr(
639                    arg,
640                    enum_name,
641                    variant_name,
642                    variant_type,
643                    module_path,
644                    function_name,
645                    specs,
646                );
647            }
648        }
649        PureExpr::MethodCall { receiver, args, .. } => {
650            find_match_in_expr(
651                receiver,
652                enum_name,
653                variant_name,
654                variant_type,
655                module_path,
656                function_name,
657                specs,
658            );
659            for arg in args {
660                find_match_in_expr(
661                    arg,
662                    enum_name,
663                    variant_name,
664                    variant_type,
665                    module_path,
666                    function_name,
667                    specs,
668                );
669            }
670        }
671        PureExpr::Binary { left, right, .. } => {
672            find_match_in_expr(
673                left,
674                enum_name,
675                variant_name,
676                variant_type,
677                module_path,
678                function_name,
679                specs,
680            );
681            find_match_in_expr(
682                right,
683                enum_name,
684                variant_name,
685                variant_type,
686                module_path,
687                function_name,
688                specs,
689            );
690        }
691        PureExpr::Unary { expr, .. } => {
692            find_match_in_expr(
693                expr,
694                enum_name,
695                variant_name,
696                variant_type,
697                module_path,
698                function_name,
699                specs,
700            );
701        }
702        PureExpr::Field { expr: base, .. } | PureExpr::Index { expr: base, .. } => {
703            find_match_in_expr(
704                base,
705                enum_name,
706                variant_name,
707                variant_type,
708                module_path,
709                function_name,
710                specs,
711            );
712        }
713        PureExpr::Ref { expr, .. }
714        | PureExpr::Try(expr)
715        | PureExpr::Return(Some(expr))
716        | PureExpr::Await(expr) => {
717            find_match_in_expr(
718                expr,
719                enum_name,
720                variant_name,
721                variant_type,
722                module_path,
723                function_name,
724                specs,
725            );
726        }
727        PureExpr::Tuple(exprs) | PureExpr::Array(exprs) => {
728            for e in exprs {
729                find_match_in_expr(
730                    e,
731                    enum_name,
732                    variant_name,
733                    variant_type,
734                    module_path,
735                    function_name,
736                    specs,
737                );
738            }
739        }
740        PureExpr::Struct { fields, .. } => {
741            for (_, field_expr) in fields {
742                find_match_in_expr(
743                    field_expr,
744                    enum_name,
745                    variant_name,
746                    variant_type,
747                    module_path,
748                    function_name,
749                    specs,
750                );
751            }
752        }
753        PureExpr::Range { start, end, .. } => {
754            if let Some(s) = start {
755                find_match_in_expr(
756                    s,
757                    enum_name,
758                    variant_name,
759                    variant_type,
760                    module_path,
761                    function_name,
762                    specs,
763                );
764            }
765            if let Some(e) = end {
766                find_match_in_expr(
767                    e,
768                    enum_name,
769                    variant_name,
770                    variant_type,
771                    module_path,
772                    function_name,
773                    specs,
774                );
775            }
776        }
777        PureExpr::Let { expr, .. } => {
778            find_match_in_expr(
779                expr,
780                enum_name,
781                variant_name,
782                variant_type,
783                module_path,
784                function_name,
785                specs,
786            );
787        }
788        PureExpr::Cast { expr, .. } => {
789            find_match_in_expr(
790                expr,
791                enum_name,
792                variant_name,
793                variant_type,
794                module_path,
795                function_name,
796                specs,
797            );
798        }
799        PureExpr::Repeat { expr, len } => {
800            find_match_in_expr(
801                expr,
802                enum_name,
803                variant_name,
804                variant_type,
805                module_path,
806                function_name,
807                specs,
808            );
809            find_match_in_expr(
810                len,
811                enum_name,
812                variant_name,
813                variant_type,
814                module_path,
815                function_name,
816                specs,
817            );
818        }
819        // Leaf expressions - no recursion needed
820        PureExpr::Lit(_)
821        | PureExpr::Path(_)
822        | PureExpr::Return(None)
823        | PureExpr::Break { .. }
824        | PureExpr::Continue { .. }
825        | PureExpr::Macro { .. }
826        | PureExpr::Other(_) => {}
827    }
828}
829
830/// Check if a pattern contains a reference to the enum.
831///
832/// Uses segment-level matching: `"FilterKind::Inclusive"` does NOT match enum `"Filter"`.
833/// Only `"Filter::Recurse"` matches because `"Filter"` is an exact `::` segment.
834pub(crate) fn pattern_contains_enum(pattern: &PurePattern, enum_name: &str) -> bool {
835    match pattern {
836        PurePattern::Path(path) => path_has_enum_segment(path, enum_name),
837        PurePattern::Struct { path, .. } => path_has_enum_segment(path, enum_name),
838        PurePattern::Or(patterns) => patterns.iter().any(|p| pattern_contains_enum(p, enum_name)),
839        PurePattern::Other(s) => {
840            let path_part = s.split(&['(', '{', ' '][..]).next().unwrap_or(s);
841            path_has_enum_segment(path_part, enum_name)
842        }
843        _ => false,
844    }
845}
846
847/// Check if a `::` separated path contains the enum name as an exact segment.
848fn path_has_enum_segment(path: &str, enum_name: &str) -> bool {
849    path.split("::").any(|segment| segment == enum_name)
850}
851
852// ============================================================================
853// Internal Helpers for Remove Cascade Analysis
854// ============================================================================
855
856/// Find match arms to remove in a block
857fn find_removable_arms_in_block(
858    block: &PureBlock,
859    enum_name: &str,
860    variant_name: &str,
861    module_path: &SymbolPath,
862    function_name: &str,
863    specs: &mut Vec<CascadeSpec>,
864) {
865    for stmt in &block.stmts {
866        match stmt {
867            PureStmt::Expr(expr) | PureStmt::Semi(expr) => {
868                find_removable_arms_in_expr(
869                    expr,
870                    enum_name,
871                    variant_name,
872                    module_path,
873                    function_name,
874                    specs,
875                );
876            }
877            PureStmt::Local { init, .. } => {
878                if let Some(init_expr) = init {
879                    find_removable_arms_in_expr(
880                        init_expr,
881                        enum_name,
882                        variant_name,
883                        module_path,
884                        function_name,
885                        specs,
886                    );
887                }
888            }
889            PureStmt::Item(_) => {}
890        }
891    }
892}
893
894/// Recursively find match arms referencing a removed variant
895fn find_removable_arms_in_expr(
896    expr: &PureExpr,
897    enum_name: &str,
898    variant_name: &str,
899    module_path: &SymbolPath,
900    function_name: &str,
901    specs: &mut Vec<CascadeSpec>,
902) {
903    match expr {
904        PureExpr::Match {
905            expr: scrutinee,
906            arms,
907        } => {
908            // Check if this match uses the target enum
909            let matches_enum = arms
910                .iter()
911                .any(|arm| pattern_contains_enum(&arm.pattern, enum_name));
912
913            if matches_enum {
914                // Find arms that reference the removed variant
915                for arm in arms {
916                    if pattern_contains_variant(&arm.pattern, enum_name, variant_name) {
917                        let pattern_str = format_pattern(&arm.pattern);
918                        specs.push(CascadeSpec::remove_match_arm(
919                            module_path.clone(),
920                            function_name,
921                            enum_name,
922                            pattern_str,
923                        ));
924                    }
925                }
926            }
927
928            // Recurse into scrutinee and arm bodies
929            find_removable_arms_in_expr(
930                scrutinee,
931                enum_name,
932                variant_name,
933                module_path,
934                function_name,
935                specs,
936            );
937            for arm in arms {
938                find_removable_arms_in_expr(
939                    &arm.body,
940                    enum_name,
941                    variant_name,
942                    module_path,
943                    function_name,
944                    specs,
945                );
946            }
947        }
948        PureExpr::Block { block, .. } => {
949            find_removable_arms_in_block(
950                block,
951                enum_name,
952                variant_name,
953                module_path,
954                function_name,
955                specs,
956            );
957        }
958        PureExpr::If {
959            cond,
960            then_branch,
961            else_branch,
962        } => {
963            find_removable_arms_in_expr(
964                cond,
965                enum_name,
966                variant_name,
967                module_path,
968                function_name,
969                specs,
970            );
971            find_removable_arms_in_block(
972                then_branch,
973                enum_name,
974                variant_name,
975                module_path,
976                function_name,
977                specs,
978            );
979            if let Some(else_expr) = else_branch {
980                find_removable_arms_in_expr(
981                    else_expr,
982                    enum_name,
983                    variant_name,
984                    module_path,
985                    function_name,
986                    specs,
987                );
988            }
989        }
990        PureExpr::Loop { body: block, .. } | PureExpr::Unsafe(block) => {
991            find_removable_arms_in_block(
992                block,
993                enum_name,
994                variant_name,
995                module_path,
996                function_name,
997                specs,
998            );
999        }
1000        PureExpr::Async { body, .. } => {
1001            find_removable_arms_in_block(
1002                body,
1003                enum_name,
1004                variant_name,
1005                module_path,
1006                function_name,
1007                specs,
1008            );
1009        }
1010        PureExpr::While { cond, body, .. } => {
1011            find_removable_arms_in_expr(
1012                cond,
1013                enum_name,
1014                variant_name,
1015                module_path,
1016                function_name,
1017                specs,
1018            );
1019            find_removable_arms_in_block(
1020                body,
1021                enum_name,
1022                variant_name,
1023                module_path,
1024                function_name,
1025                specs,
1026            );
1027        }
1028        PureExpr::For {
1029            expr: iter, body, ..
1030        } => {
1031            find_removable_arms_in_expr(
1032                iter,
1033                enum_name,
1034                variant_name,
1035                module_path,
1036                function_name,
1037                specs,
1038            );
1039            find_removable_arms_in_block(
1040                body,
1041                enum_name,
1042                variant_name,
1043                module_path,
1044                function_name,
1045                specs,
1046            );
1047        }
1048        PureExpr::Closure { body, .. } => {
1049            find_removable_arms_in_expr(
1050                body,
1051                enum_name,
1052                variant_name,
1053                module_path,
1054                function_name,
1055                specs,
1056            );
1057        }
1058        PureExpr::Call { func, args } => {
1059            find_removable_arms_in_expr(
1060                func,
1061                enum_name,
1062                variant_name,
1063                module_path,
1064                function_name,
1065                specs,
1066            );
1067            for arg in args {
1068                find_removable_arms_in_expr(
1069                    arg,
1070                    enum_name,
1071                    variant_name,
1072                    module_path,
1073                    function_name,
1074                    specs,
1075                );
1076            }
1077        }
1078        PureExpr::MethodCall { receiver, args, .. } => {
1079            find_removable_arms_in_expr(
1080                receiver,
1081                enum_name,
1082                variant_name,
1083                module_path,
1084                function_name,
1085                specs,
1086            );
1087            for arg in args {
1088                find_removable_arms_in_expr(
1089                    arg,
1090                    enum_name,
1091                    variant_name,
1092                    module_path,
1093                    function_name,
1094                    specs,
1095                );
1096            }
1097        }
1098        PureExpr::Binary { left, right, .. } => {
1099            find_removable_arms_in_expr(
1100                left,
1101                enum_name,
1102                variant_name,
1103                module_path,
1104                function_name,
1105                specs,
1106            );
1107            find_removable_arms_in_expr(
1108                right,
1109                enum_name,
1110                variant_name,
1111                module_path,
1112                function_name,
1113                specs,
1114            );
1115        }
1116        PureExpr::Unary { expr, .. }
1117        | PureExpr::Field { expr, .. }
1118        | PureExpr::Index { expr, .. }
1119        | PureExpr::Ref { expr, .. }
1120        | PureExpr::Try(expr)
1121        | PureExpr::Await(expr)
1122        | PureExpr::Let { expr, .. }
1123        | PureExpr::Cast { expr, .. } => {
1124            find_removable_arms_in_expr(
1125                expr,
1126                enum_name,
1127                variant_name,
1128                module_path,
1129                function_name,
1130                specs,
1131            );
1132        }
1133        PureExpr::Return(Some(expr)) => {
1134            find_removable_arms_in_expr(
1135                expr,
1136                enum_name,
1137                variant_name,
1138                module_path,
1139                function_name,
1140                specs,
1141            );
1142        }
1143        PureExpr::Tuple(exprs) | PureExpr::Array(exprs) => {
1144            for e in exprs {
1145                find_removable_arms_in_expr(
1146                    e,
1147                    enum_name,
1148                    variant_name,
1149                    module_path,
1150                    function_name,
1151                    specs,
1152                );
1153            }
1154        }
1155        PureExpr::Struct { fields, .. } => {
1156            for (_, field_expr) in fields {
1157                find_removable_arms_in_expr(
1158                    field_expr,
1159                    enum_name,
1160                    variant_name,
1161                    module_path,
1162                    function_name,
1163                    specs,
1164                );
1165            }
1166        }
1167        PureExpr::Range { start, end, .. } => {
1168            if let Some(s) = start {
1169                find_removable_arms_in_expr(
1170                    s,
1171                    enum_name,
1172                    variant_name,
1173                    module_path,
1174                    function_name,
1175                    specs,
1176                );
1177            }
1178            if let Some(e) = end {
1179                find_removable_arms_in_expr(
1180                    e,
1181                    enum_name,
1182                    variant_name,
1183                    module_path,
1184                    function_name,
1185                    specs,
1186                );
1187            }
1188        }
1189        PureExpr::Repeat { expr, len } => {
1190            find_removable_arms_in_expr(
1191                expr,
1192                enum_name,
1193                variant_name,
1194                module_path,
1195                function_name,
1196                specs,
1197            );
1198            find_removable_arms_in_expr(
1199                len,
1200                enum_name,
1201                variant_name,
1202                module_path,
1203                function_name,
1204                specs,
1205            );
1206        }
1207        // Leaf expressions
1208        PureExpr::Lit(_)
1209        | PureExpr::Path(_)
1210        | PureExpr::Return(None)
1211        | PureExpr::Break { .. }
1212        | PureExpr::Continue { .. }
1213        | PureExpr::Macro { .. }
1214        | PureExpr::Other(_) => {}
1215    }
1216}
1217
1218/// Check if a pattern references a specific variant of an enum
1219fn pattern_contains_variant(pattern: &PurePattern, enum_name: &str, variant_name: &str) -> bool {
1220    let target = format!("{}::{}", enum_name, variant_name);
1221    match pattern {
1222        PurePattern::Path(path) => path.contains(&target),
1223        PurePattern::Struct { path, .. } => path.contains(&target),
1224        PurePattern::Or(patterns) => patterns
1225            .iter()
1226            .any(|p| pattern_contains_variant(p, enum_name, variant_name)),
1227        _ => false,
1228    }
1229}
1230
1231/// Format a PurePattern back to a string for RemoveMatchArm
1232fn format_pattern(pattern: &PurePattern) -> String {
1233    match pattern {
1234        PurePattern::Path(path) => path.clone(),
1235        PurePattern::Wild => "_".to_string(),
1236        PurePattern::Ident { name, .. } => name.clone(),
1237        PurePattern::Struct {
1238            path, fields, rest, ..
1239        } => {
1240            let field_strs: Vec<String> = fields
1241                .iter()
1242                .map(|(name, pat)| {
1243                    let pat_str = format_pattern(pat);
1244                    if pat_str == *name {
1245                        name.clone()
1246                    } else {
1247                        format!("{}: {}", name, pat_str)
1248                    }
1249                })
1250                .collect();
1251            if *rest {
1252                format!("{} {{ {}, .. }}", path, field_strs.join(", "))
1253            } else {
1254                format!("{} {{ {} }}", path, field_strs.join(", "))
1255            }
1256        }
1257        PurePattern::Tuple(patterns) => {
1258            let inner: Vec<String> = patterns.iter().map(format_pattern).collect();
1259            format!("({})", inner.join(", "))
1260        }
1261        PurePattern::Or(patterns) => {
1262            let inner: Vec<String> = patterns.iter().map(format_pattern).collect();
1263            inner.join(" | ")
1264        }
1265        PurePattern::Lit(lit) => lit.clone(),
1266        PurePattern::Rest => "..".to_string(),
1267        PurePattern::Ref {
1268            is_mut, pattern, ..
1269        } => {
1270            let inner = format_pattern(pattern);
1271            if *is_mut {
1272                format!("&mut {}", inner)
1273            } else {
1274                format!("&{}", inner)
1275            }
1276        }
1277        PurePattern::Range {
1278            start,
1279            end,
1280            inclusive,
1281        } => {
1282            let s = start.as_deref().unwrap_or("");
1283            let e = end.as_deref().unwrap_or("");
1284            if *inclusive {
1285                format!("{}..={}", s, e)
1286            } else {
1287                format!("{}..{}", s, e)
1288            }
1289        }
1290        PurePattern::Slice(patterns) => {
1291            let inner: Vec<String> = patterns.iter().map(format_pattern).collect();
1292            format!("[{}]", inner.join(", "))
1293        }
1294        PurePattern::Other(s) => s.clone(),
1295    }
1296}
1297
1298// ============================================================================
1299// Tests
1300// ============================================================================
1301
1302#[cfg(test)]
1303mod tests {
1304    use super::*;
1305    use std::fs;
1306    use tempfile::tempdir;
1307
1308    /// Create a test project with enum and match expressions
1309    fn create_test_project_with_enum() -> tempfile::TempDir {
1310        let dir = tempdir().unwrap();
1311        let src = dir.path().join("src");
1312        fs::create_dir(&src).unwrap();
1313
1314        // Create Cargo.toml for WorkspaceResolver
1315        fs::write(
1316            dir.path().join("Cargo.toml"),
1317            r#"[package]
1318name = "test_enum_project"
1319version = "0.1.0"
1320edition = "2021"
1321"#,
1322        )
1323        .unwrap();
1324
1325        fs::write(
1326            src.join("lib.rs"),
1327            r#"
1328pub enum Status {
1329    Active,
1330    Inactive,
1331    Pending,
1332}
1333
1334pub fn handle_status(status: Status) -> &'static str {
1335    match status {
1336        Status::Active => "active",
1337        Status::Inactive => "inactive",
1338        Status::Pending => "pending",
1339    }
1340}
1341
1342pub struct User {
1343    pub name: String,
1344    pub status: Status,
1345}
1346
1347impl User {
1348    pub fn status_label(&self) -> &'static str {
1349        match self.status {
1350            Status::Active => "A",
1351            Status::Inactive => "I",
1352            Status::Pending => "P",
1353        }
1354    }
1355}
1356"#,
1357        )
1358        .unwrap();
1359
1360        dir
1361    }
1362
1363    /// Create a test project for symbol discovery
1364    fn create_test_project_for_discovery() -> tempfile::TempDir {
1365        let dir = tempdir().unwrap();
1366        let src = dir.path().join("src");
1367        fs::create_dir(&src).unwrap();
1368
1369        // Create Cargo.toml for WorkspaceResolver
1370        fs::write(
1371            dir.path().join("Cargo.toml"),
1372            r#"[package]
1373name = "test_project"
1374version = "0.1.0"
1375edition = "2021"
1376"#,
1377        )
1378        .unwrap();
1379
1380        fs::write(
1381            src.join("lib.rs"),
1382            r#"
1383pub fn foo() {}
1384pub fn bar() {}
1385pub fn foobar() {}
1386
1387pub struct Foo;
1388pub struct Bar;
1389
1390pub enum MyEnum {
1391    Variant1,
1392    Variant2,
1393}
1394
1395pub trait MyTrait {
1396    fn do_something(&self);
1397}
1398"#,
1399        )
1400        .unwrap();
1401
1402        dir
1403    }
1404
1405    // ------------------------------------------------------------------------
1406    // DiscoverService Construction Tests
1407    // ------------------------------------------------------------------------
1408
1409    #[test]
1410    fn test_from_path() {
1411        let dir = create_test_project_for_discovery();
1412        let result = DiscoverService::from_path(dir.path());
1413        assert!(result.is_ok());
1414    }
1415
1416    #[test]
1417    fn test_from_path_nonexistent() {
1418        let result = DiscoverService::from_path(Path::new("/nonexistent/path"));
1419        assert!(result.is_err());
1420    }
1421
1422    #[test]
1423    fn test_from_project() {
1424        let dir = create_test_project_for_discovery();
1425        let project = Project::load(dir.path()).unwrap();
1426        let service = DiscoverService::from_project(&project).unwrap();
1427        assert!(!service.registry().is_empty());
1428    }
1429
1430    // ------------------------------------------------------------------------
1431    // Symbol Discovery Tests
1432    // ------------------------------------------------------------------------
1433
1434    #[test]
1435    fn test_find_symbols_exact_match() {
1436        let dir = create_test_project_for_discovery();
1437        let service = DiscoverService::from_path(dir.path()).unwrap();
1438
1439        let result = service.find_symbols("foo");
1440        assert!(!result.symbols.is_empty());
1441        assert!(result.symbols.iter().any(|s| s.path.name() == "foo"));
1442    }
1443
1444    #[test]
1445    fn test_find_symbols_wildcard() {
1446        let dir = create_test_project_for_discovery();
1447        let service = DiscoverService::from_path(dir.path()).unwrap();
1448
1449        let result = service.find_symbols("*foo*");
1450        // Should match: foo, foobar, Foo
1451        assert!(result.symbols.len() >= 2);
1452    }
1453
1454    #[test]
1455    fn test_find_symbols_by_kind_function() {
1456        let dir = create_test_project_for_discovery();
1457        let service = DiscoverService::from_path(dir.path()).unwrap();
1458
1459        let result = service.find_symbols_by_kind("*", SymbolKind::Function);
1460        // Should find foo, bar, foobar
1461        assert!(result
1462            .symbols
1463            .iter()
1464            .all(|s| s.kind == SymbolKind::Function));
1465    }
1466
1467    #[test]
1468    fn test_find_symbols_by_kind_struct() {
1469        let dir = create_test_project_for_discovery();
1470        let service = DiscoverService::from_path(dir.path()).unwrap();
1471
1472        let result = service.find_symbols_by_kind("*", SymbolKind::Struct);
1473        assert!(result.symbols.iter().all(|s| s.kind == SymbolKind::Struct));
1474    }
1475
1476    #[test]
1477    fn test_find_symbols_by_kind_enum() {
1478        let dir = create_test_project_for_discovery();
1479        let service = DiscoverService::from_path(dir.path()).unwrap();
1480
1481        let result = service.find_symbols_by_kind("*", SymbolKind::Enum);
1482        assert!(!result.symbols.is_empty());
1483        assert!(result.symbols.iter().any(|s| s.path.name() == "MyEnum"));
1484    }
1485
1486    #[test]
1487    fn test_find_symbols_no_match() {
1488        let dir = create_test_project_for_discovery();
1489        let service = DiscoverService::from_path(dir.path()).unwrap();
1490
1491        let result = service.find_symbols("nonexistent_symbol");
1492        assert!(result.symbols.is_empty());
1493    }
1494
1495    // ------------------------------------------------------------------------
1496    // Cascade Effect Tests
1497    // ------------------------------------------------------------------------
1498
1499    #[test]
1500    fn test_find_cascade_effects_basic() {
1501        let dir = create_test_project_with_enum();
1502        let service = DiscoverService::from_path(dir.path()).unwrap();
1503
1504        let result = service.find_cascade_effects("Status", Some("Cancelled"), None);
1505        assert_eq!(result.symbol, "Status");
1506        // Should find 2 match expressions (handle_status and status_label)
1507        assert_eq!(result.len(), 2);
1508    }
1509
1510    #[test]
1511    fn test_find_cascade_effects_default_variant() {
1512        let dir = create_test_project_with_enum();
1513        let service = DiscoverService::from_path(dir.path()).unwrap();
1514
1515        let result = service.find_cascade_effects("Status", None, None);
1516        // Default variant name should be "NewVariant"
1517        assert!(!result.is_empty());
1518    }
1519
1520    #[test]
1521    fn test_find_cascade_effects_no_match() {
1522        let dir = create_test_project_with_enum();
1523        let service = DiscoverService::from_path(dir.path()).unwrap();
1524
1525        let result = service.find_cascade_effects("NonExistentEnum", Some("Variant"), None);
1526        assert!(result.is_empty());
1527    }
1528
1529    #[test]
1530    fn test_find_cascade_effects_wildcard_stripped() {
1531        let dir = create_test_project_with_enum();
1532        let service = DiscoverService::from_path(dir.path()).unwrap();
1533
1534        // Wildcards should be stripped
1535        let result = service.find_cascade_effects("*Status*", Some("Cancelled"), None);
1536        assert!(!result.is_empty());
1537    }
1538
1539    #[test]
1540    fn test_cascade_spec_target_paths_include_function() {
1541        // Regression: CascadeSpec.target was module path only, causing
1542        // resolve_target_from_3fields to resolve to the module symbol
1543        // instead of the function/method.
1544        let dir = create_test_project_with_enum();
1545        let service = DiscoverService::from_path(dir.path()).unwrap();
1546
1547        let result = service.find_cascade_effects("Status", Some("Cancelled"), None);
1548        assert_eq!(result.len(), 2, "Expected 2 cascade specs");
1549
1550        // Collect (target, function_name) from each spec
1551        let mut entries: Vec<(String, String)> = result
1552            .specs
1553            .iter()
1554            .map(|spec| match spec {
1555                CascadeSpec::AddMatchArm {
1556                    target,
1557                    function_name,
1558                    ..
1559                } => (target.to_string(), function_name.clone()),
1560                other => panic!("Expected AddMatchArm, got {:?}", other),
1561            })
1562            .collect();
1563        entries.sort();
1564
1565        // Free function: target should be the module, function_name is the fn name
1566        assert!(
1567            entries.iter().any(|(_t, f)| f == "handle_status"),
1568            "Should have handle_status cascade: {:?}",
1569            entries
1570        );
1571
1572        // Method: target should include the type name (User), not just the module
1573        let method_entry = entries
1574            .iter()
1575            .find(|(_, f)| f == "status_label")
1576            .expect("Should have status_label cascade");
1577        assert!(
1578            method_entry.0.contains("User"),
1579            "Method target should include type name 'User', got: {}",
1580            method_entry.0
1581        );
1582    }
1583
1584    #[test]
1585    fn test_cascade_to_intent_constructs_full_path() {
1586        // Regression: CascadeSpec → Intent conversion put module path in symbol_path
1587        // and function_name in target_fn, but target_fn was ignored by the planner.
1588        use crate::intent::Intent;
1589
1590        let dir = create_test_project_with_enum();
1591        let service = DiscoverService::from_path(dir.path()).unwrap();
1592
1593        let result = service.find_cascade_effects("Status", Some("Cancelled"), None);
1594        assert_eq!(result.len(), 2);
1595
1596        let intents: Vec<Intent> = result.specs.into_iter().map(Intent::from).collect();
1597
1598        for intent in &intents {
1599            match intent {
1600                Intent::AddMatchArm {
1601                    symbol_path,
1602                    target_fn,
1603                    ..
1604                } => {
1605                    let path = symbol_path.as_ref().expect("symbol_path should be set");
1606                    // symbol_path should contain the full function/method path
1607                    assert!(
1608                        path.contains("handle_status") || path.contains("status_label"),
1609                        "symbol_path should contain function name, got: {}",
1610                        path
1611                    );
1612                    // target_fn should be None (already included in symbol_path)
1613                    assert!(
1614                        target_fn.is_none(),
1615                        "target_fn should be None when symbol_path has full path"
1616                    );
1617                }
1618                other => panic!("Expected AddMatchArm intent, got {:?}", other),
1619            }
1620        }
1621
1622        // Verify the method path includes the type name
1623        let method_intent = intents.iter().find(|i| {
1624            matches!(i, Intent::AddMatchArm { symbol_path: Some(p), .. } if p.contains("status_label"))
1625        }).expect("Should have status_label intent");
1626
1627        if let Intent::AddMatchArm {
1628            symbol_path: Some(path),
1629            ..
1630        } = method_intent
1631        {
1632            assert!(
1633                path.contains("User"),
1634                "Method symbol_path should include type name, got: {}",
1635                path
1636            );
1637        }
1638    }
1639
1640    // ------------------------------------------------------------------------
1641    // generate_match_pattern Tests
1642    // ------------------------------------------------------------------------
1643
1644    #[test]
1645    fn test_generate_match_pattern_unit() {
1646        assert_eq!(
1647            generate_match_pattern("Status", "Active", "unit"),
1648            "Status::Active"
1649        );
1650        assert_eq!(
1651            generate_match_pattern("Status", "Active", ""),
1652            "Status::Active"
1653        );
1654    }
1655
1656    #[test]
1657    fn test_generate_match_pattern_tuple() {
1658        assert_eq!(
1659            generate_match_pattern("Result", "Ok", "tuple:T"),
1660            "Result::Ok(_)"
1661        );
1662        assert_eq!(
1663            generate_match_pattern("Option", "Some", "tuple:i32,String"),
1664            "Option::Some(_, _)"
1665        );
1666        assert_eq!(
1667            generate_match_pattern("PathSegment", "Slice", "tuple:Option<usize>,Option<usize>"),
1668            "PathSegment::Slice(_, _)"
1669        );
1670    }
1671
1672    #[test]
1673    fn test_generate_match_pattern_struct() {
1674        assert_eq!(
1675            generate_match_pattern(
1676                "PathSegment",
1677                "Slice",
1678                "struct:start:Option<usize>,end:Option<usize>"
1679            ),
1680            "PathSegment::Slice { start: _, end: _ }"
1681        );
1682        assert_eq!(
1683            generate_match_pattern("Error", "Custom", "struct:code:u32"),
1684            "Error::Custom { code: _ }"
1685        );
1686    }
1687
1688    #[test]
1689    fn test_generate_match_pattern_struct_empty() {
1690        // Edge case: struct with no fields
1691        assert_eq!(
1692            generate_match_pattern("Foo", "Bar", "struct:"),
1693            "Foo::Bar { .. }"
1694        );
1695    }
1696
1697    // ------------------------------------------------------------------------
1698    // pattern_contains_enum Tests
1699    // ------------------------------------------------------------------------
1700
1701    #[test]
1702    fn test_pattern_contains_enum_path() {
1703        let pattern = PurePattern::Path("Status::Active".to_string());
1704        assert!(pattern_contains_enum(&pattern, "Status"));
1705        assert!(!pattern_contains_enum(&pattern, "Other"));
1706    }
1707
1708    #[test]
1709    fn test_pattern_contains_enum_struct() {
1710        let pattern = PurePattern::Struct {
1711            path: "MyStruct".to_string(),
1712            fields: vec![],
1713            rest: false,
1714        };
1715        assert!(pattern_contains_enum(&pattern, "MyStruct"));
1716        assert!(!pattern_contains_enum(&pattern, "Other"));
1717    }
1718
1719    #[test]
1720    fn test_pattern_contains_enum_or() {
1721        let pattern = PurePattern::Or(vec![
1722            PurePattern::Path("Status::Active".to_string()),
1723            PurePattern::Path("Status::Inactive".to_string()),
1724        ]);
1725        assert!(pattern_contains_enum(&pattern, "Status"));
1726        assert!(!pattern_contains_enum(&pattern, "Other"));
1727    }
1728
1729    #[test]
1730    fn test_pattern_contains_enum_wild() {
1731        let pattern = PurePattern::Wild;
1732        assert!(!pattern_contains_enum(&pattern, "Status"));
1733    }
1734
1735    // ------------------------------------------------------------------------
1736    // CascadeResult Tests
1737    // ------------------------------------------------------------------------
1738
1739    #[test]
1740    fn test_cascade_result_new() {
1741        let result = CascadeResult::new("TestEnum".to_string());
1742        assert_eq!(result.symbol, "TestEnum");
1743        assert!(result.is_empty());
1744        assert_eq!(result.len(), 0);
1745    }
1746
1747    // ------------------------------------------------------------------------
1748    // count_tuple_elements Tests
1749    // ------------------------------------------------------------------------
1750
1751    #[test]
1752    fn test_count_tuple_elements_empty() {
1753        assert_eq!(count_tuple_elements(""), 0);
1754    }
1755
1756    #[test]
1757    fn test_count_tuple_elements_single() {
1758        assert_eq!(count_tuple_elements("i32"), 1);
1759        assert_eq!(count_tuple_elements("String"), 1);
1760    }
1761
1762    #[test]
1763    fn test_count_tuple_elements_multiple() {
1764        assert_eq!(count_tuple_elements("i32,String"), 2);
1765        assert_eq!(count_tuple_elements("i32,i32,i32"), 3);
1766    }
1767
1768    #[test]
1769    fn test_count_tuple_elements_with_generics() {
1770        assert_eq!(count_tuple_elements("Option<usize>"), 1);
1771        assert_eq!(count_tuple_elements("Option<usize>,Option<usize>"), 2);
1772        assert_eq!(count_tuple_elements("HashMap<K, V>,String"), 2);
1773        assert_eq!(count_tuple_elements("Result<Vec<i32>, Error>"), 1);
1774    }
1775
1776    #[test]
1777    fn test_count_tuple_elements_nested_generics() {
1778        assert_eq!(count_tuple_elements("Option<Result<i32, E>>,String"), 2);
1779        assert_eq!(count_tuple_elements("Vec<HashMap<K, V>>,Option<T>"), 2);
1780    }
1781
1782    // ========================================================================
1783    // pattern_contains_variant tests
1784    // ========================================================================
1785
1786    #[test]
1787    fn test_pattern_contains_variant_path_match() {
1788        let pat = PurePattern::Path("Status::Active".to_string());
1789        assert!(pattern_contains_variant(&pat, "Status", "Active"));
1790    }
1791
1792    #[test]
1793    fn test_pattern_contains_variant_path_no_match() {
1794        let pat = PurePattern::Path("Status::Active".to_string());
1795        assert!(!pattern_contains_variant(&pat, "Status", "Inactive"));
1796    }
1797
1798    #[test]
1799    fn test_pattern_contains_variant_struct_pattern() {
1800        let pat = PurePattern::Struct {
1801            path: "Status::Active".to_string(),
1802            fields: vec![],
1803            rest: false,
1804        };
1805        assert!(pattern_contains_variant(&pat, "Status", "Active"));
1806    }
1807
1808    #[test]
1809    fn test_pattern_contains_variant_or_pattern() {
1810        let pat = PurePattern::Or(vec![
1811            PurePattern::Path("Status::Active".to_string()),
1812            PurePattern::Path("Status::Pending".to_string()),
1813        ]);
1814        assert!(pattern_contains_variant(&pat, "Status", "Active"));
1815        assert!(pattern_contains_variant(&pat, "Status", "Pending"));
1816        assert!(!pattern_contains_variant(&pat, "Status", "Inactive"));
1817    }
1818
1819    #[test]
1820    fn test_pattern_contains_variant_wildcard() {
1821        let pat = PurePattern::Wild;
1822        assert!(!pattern_contains_variant(&pat, "Status", "Active"));
1823    }
1824
1825    // ========================================================================
1826    // format_pattern tests
1827    // ========================================================================
1828
1829    #[test]
1830    fn test_format_pattern_path() {
1831        let pat = PurePattern::Path("Status::Active".to_string());
1832        assert_eq!(format_pattern(&pat), "Status::Active");
1833    }
1834
1835    #[test]
1836    fn test_format_pattern_wild() {
1837        assert_eq!(format_pattern(&PurePattern::Wild), "_");
1838    }
1839
1840    #[test]
1841    fn test_format_pattern_ident() {
1842        let pat = PurePattern::Ident {
1843            name: "x".to_string(),
1844            is_mut: false,
1845        };
1846        assert_eq!(format_pattern(&pat), "x");
1847    }
1848
1849    #[test]
1850    fn test_format_pattern_struct_with_rest() {
1851        let pat = PurePattern::Struct {
1852            path: "Point".to_string(),
1853            fields: vec![(
1854                "x".to_string(),
1855                PurePattern::Ident {
1856                    name: "x".to_string(),
1857                    is_mut: false,
1858                },
1859            )],
1860            rest: true,
1861        };
1862        assert_eq!(format_pattern(&pat), "Point { x, .. }");
1863    }
1864
1865    #[test]
1866    fn test_format_pattern_tuple() {
1867        let pat = PurePattern::Tuple(vec![
1868            PurePattern::Path("A".to_string()),
1869            PurePattern::Path("B".to_string()),
1870        ]);
1871        assert_eq!(format_pattern(&pat), "(A, B)");
1872    }
1873
1874    #[test]
1875    fn test_format_pattern_or() {
1876        let pat = PurePattern::Or(vec![
1877            PurePattern::Path("A".to_string()),
1878            PurePattern::Path("B".to_string()),
1879        ]);
1880        assert_eq!(format_pattern(&pat), "A | B");
1881    }
1882
1883    #[test]
1884    fn test_format_pattern_ref() {
1885        let pat = PurePattern::Ref {
1886            is_mut: false,
1887            pattern: Box::new(PurePattern::Ident {
1888                name: "x".to_string(),
1889                is_mut: false,
1890            }),
1891        };
1892        assert_eq!(format_pattern(&pat), "&x");
1893    }
1894
1895    #[test]
1896    fn test_format_pattern_ref_mut() {
1897        let pat = PurePattern::Ref {
1898            is_mut: true,
1899            pattern: Box::new(PurePattern::Ident {
1900                name: "y".to_string(),
1901                is_mut: false,
1902            }),
1903        };
1904        assert_eq!(format_pattern(&pat), "&mut y");
1905    }
1906
1907    #[test]
1908    fn test_format_pattern_range_inclusive() {
1909        let pat = PurePattern::Range {
1910            start: Some("1".to_string()),
1911            end: Some("5".to_string()),
1912            inclusive: true,
1913        };
1914        assert_eq!(format_pattern(&pat), "1..=5");
1915    }
1916
1917    #[test]
1918    fn test_format_pattern_slice() {
1919        let pat = PurePattern::Slice(vec![
1920            PurePattern::Ident {
1921                name: "first".to_string(),
1922                is_mut: false,
1923            },
1924            PurePattern::Rest,
1925        ]);
1926        assert_eq!(format_pattern(&pat), "[first, ..]");
1927    }
1928
1929    // ========================================================================
1930    // find_remove_cascade_effects integration test
1931    // ========================================================================
1932
1933    #[test]
1934    fn test_find_remove_cascade_effects_finds_matching_arms() {
1935        let dir = create_test_project_with_enum();
1936        let service = DiscoverService::from_path(dir.path()).unwrap();
1937
1938        let result = service.find_remove_cascade_effects("Status", "Pending");
1939
1940        // Should find match arms for Status::Pending in both handle_status and status_label
1941        assert!(
1942            result.specs.len() >= 2,
1943            "Expected at least 2 RemoveMatchArm specs, got {}: {:?}",
1944            result.specs.len(),
1945            result.specs
1946        );
1947
1948        // All specs should be RemoveMatchArm
1949        for spec in &result.specs {
1950            match spec {
1951                CascadeSpec::RemoveMatchArm {
1952                    enum_name, pattern, ..
1953                } => {
1954                    assert_eq!(enum_name, "Status");
1955                    assert!(
1956                        pattern.contains("Pending"),
1957                        "Pattern should contain 'Pending': {}",
1958                        pattern
1959                    );
1960                }
1961                other => panic!("Expected RemoveMatchArm, got {:?}", other),
1962            }
1963        }
1964    }
1965
1966    #[test]
1967    fn test_remove_cascade_target_paths_include_type_for_methods() {
1968        // Regression: RemoveMatchArm target should include type name for methods
1969        let dir = create_test_project_with_enum();
1970        let service = DiscoverService::from_path(dir.path()).unwrap();
1971
1972        let result = service.find_remove_cascade_effects("Status", "Pending");
1973        assert!(result.specs.len() >= 2);
1974
1975        // Check that method target includes the type name
1976        let method_spec = result.specs.iter().find(|spec| {
1977            matches!(spec, CascadeSpec::RemoveMatchArm { function_name, .. } if function_name == "status_label")
1978        }).expect("Should find status_label RemoveMatchArm");
1979
1980        if let CascadeSpec::RemoveMatchArm { target, .. } = method_spec {
1981            assert!(
1982                target.to_string().contains("User"),
1983                "Method target should include 'User', got: {}",
1984                target
1985            );
1986        }
1987
1988        // Verify Intent conversion constructs full path
1989        use crate::intent::Intent;
1990        let intents: Vec<Intent> = result.specs.into_iter().map(Intent::from).collect();
1991        let method_intent = intents.iter().find(|i| {
1992            matches!(i, Intent::RemoveMatchArm { symbol_path: Some(p), .. } if p.contains("status_label"))
1993        }).expect("Should have status_label RemoveMatchArm intent");
1994
1995        if let Intent::RemoveMatchArm {
1996            symbol_path: Some(path),
1997            target_fn,
1998            ..
1999        } = method_intent
2000        {
2001            assert!(
2002                path.contains("User") && path.contains("status_label"),
2003                "Full method path should contain both User and status_label, got: {}",
2004                path
2005            );
2006            assert!(target_fn.is_none(), "target_fn should be None");
2007        }
2008    }
2009
2010    #[test]
2011    fn test_find_remove_cascade_effects_no_match_for_nonexistent_variant() {
2012        let dir = create_test_project_with_enum();
2013        let service = DiscoverService::from_path(dir.path()).unwrap();
2014
2015        let result = service.find_remove_cascade_effects("Status", "NonExistent");
2016
2017        assert!(
2018            result.specs.is_empty(),
2019            "Expected no specs for nonexistent variant, got {}: {:?}",
2020            result.specs.len(),
2021            result.specs
2022        );
2023    }
2024
2025    // ----------------------------------------------------------------
2026    // path_has_enum_segment tests
2027    // ----------------------------------------------------------------
2028
2029    #[test]
2030    fn test_path_segment_exact() {
2031        assert!(path_has_enum_segment("Filter::Recurse", "Filter"));
2032        assert!(path_has_enum_segment("Filter", "Filter"));
2033    }
2034
2035    #[test]
2036    fn test_path_segment_no_substring() {
2037        assert!(!path_has_enum_segment("FilterKind::Inclusive", "Filter"));
2038    }
2039
2040    #[test]
2041    fn test_pattern_contains_enum_segment_level() {
2042        let pat = PurePattern::Path("FilterKind::Inclusive".to_string());
2043        // "FilterKind" is not "Filter" — substring match rejected
2044        assert!(!pattern_contains_enum(&pat, "Filter"));
2045        // But "FilterKind" matches exactly
2046        assert!(pattern_contains_enum(&pat, "FilterKind"));
2047    }
2048}