1use 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
21pub struct DiscoverService {
27 ctx: AnalysisContext,
28}
29
30impl DiscoverService {
31 pub fn new(ctx: AnalysisContext) -> Self {
35 Self { ctx }
36 }
37
38 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 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 pub fn context(&self) -> &AnalysisContext {
56 &self.ctx
57 }
58
59 pub fn registry(&self) -> &SymbolRegistry {
61 &self.ctx.registry
62 }
63
64 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 pub fn find_symbols(&self, pattern: &str) -> DiscoveryResult {
72 let query = DiscoveryQuery::symbol(pattern);
73 self.discover(&query)
74 }
75
76 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 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 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 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
114pub 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 {
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
210pub 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
266fn wfp_to_symbol_path(wfp: &ryo_symbol::WorkspaceFilePath) -> SymbolPath {
272 let path_str = wfp.as_relative().to_string_lossy();
275 let crate_name = wfp.crate_name();
276
277 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 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 let module_path = relative.trim_end_matches(".rs").replace('/', "::");
293
294 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
313fn impl_symbol_path(module_path: &SymbolPath, impl_block: &PureImpl) -> SymbolPath {
321 let segment = if let Some(trait_name) = &impl_block.trait_ {
322 format!("<impl {} for {}>", trait_name, impl_block.self_ty)
324 } else {
325 impl_block.self_ty.clone()
327 };
328
329 module_path
330 .child(&segment)
331 .unwrap_or_else(|_| module_path.clone())
332}
333
334fn 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 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 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 format!("{}::{}", enum_name, variant_name)
380 }
381}
382
383fn count_tuple_elements(types_str: &str) -> usize {
391 if types_str.is_empty() {
392 return 0;
393 }
394
395 let mut count = 1usize; 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
410fn 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
451fn 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 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 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 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 PureExpr::Lit(_)
821 | PureExpr::Path(_)
822 | PureExpr::Return(None)
823 | PureExpr::Break { .. }
824 | PureExpr::Continue { .. }
825 | PureExpr::Macro { .. }
826 | PureExpr::Other(_) => {}
827 }
828}
829
830pub(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
847fn path_has_enum_segment(path: &str, enum_name: &str) -> bool {
849 path.split("::").any(|segment| segment == enum_name)
850}
851
852fn 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
894fn 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 let matches_enum = arms
910 .iter()
911 .any(|arm| pattern_contains_enum(&arm.pattern, enum_name));
912
913 if matches_enum {
914 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 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 PureExpr::Lit(_)
1209 | PureExpr::Path(_)
1210 | PureExpr::Return(None)
1211 | PureExpr::Break { .. }
1212 | PureExpr::Continue { .. }
1213 | PureExpr::Macro { .. }
1214 | PureExpr::Other(_) => {}
1215 }
1216}
1217
1218fn 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
1231fn 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#[cfg(test)]
1303mod tests {
1304 use super::*;
1305 use std::fs;
1306 use tempfile::tempdir;
1307
1308 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 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 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 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 #[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 #[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 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 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 #[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 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 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 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 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 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 assert!(
1567 entries.iter().any(|(_t, f)| f == "handle_status"),
1568 "Should have handle_status cascade: {:?}",
1569 entries
1570 );
1571
1572 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 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 assert!(
1608 path.contains("handle_status") || path.contains("status_label"),
1609 "symbol_path should contain function name, got: {}",
1610 path
1611 );
1612 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 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 #[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 assert_eq!(
1692 generate_match_pattern("Foo", "Bar", "struct:"),
1693 "Foo::Bar { .. }"
1694 );
1695 }
1696
1697 #[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 #[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 #[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 #[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 #[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 #[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 assert!(
1942 result.specs.len() >= 2,
1943 "Expected at least 2 RemoveMatchArm specs, got {}: {:?}",
1944 result.specs.len(),
1945 result.specs
1946 );
1947
1948 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 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 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 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 #[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 assert!(!pattern_contains_enum(&pat, "Filter"));
2045 assert!(pattern_contains_enum(&pat, "FilterKind"));
2047 }
2048}