1use super::blueprint::ParallelBlueprint;
40use super::registry::MutationRegistry;
41use super::spec::{MutationSpec, MutationTargetSymbol, StmtInsertPosition};
42use crate::engine::{collect_affected_ids, MutationEvent};
43use ryo_analysis::{AnalysisContext, RegistryUpdateBatch, SymbolPath};
44use ryo_mutations::MutationResult;
45use ryo_source::pure::ToSynError;
46use ryo_symbol::{MetadataError, SymbolId, WorkspaceFilePath};
47use std::collections::HashSet;
48use std::sync::Arc;
49use tracing::{debug, info, instrument, warn};
50
51#[derive(Debug, thiserror::Error)]
53pub enum SyncError {
54 #[error("cargo metadata unavailable: {0}")]
55 Metadata(#[from] MetadataError),
56 #[error("source generation failed: {0}")]
57 SourceGeneration(#[from] ToSynError),
58}
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
62pub enum ExecutionStrategy {
63 #[default]
65 Sequential,
66
67 Wavefront,
70}
71
72pub fn suggest_strategy(blueprint: &ParallelBlueprint) -> ExecutionStrategy {
74 let parallelism = blueprint.parallelism();
75 let mutation_count = blueprint.mutations.len();
76
77 if parallelism <= 1.2 || mutation_count <= 2 {
82 ExecutionStrategy::Sequential
83 } else {
84 ExecutionStrategy::Wavefront
85 }
86}
87
88#[derive(Debug, Clone)]
90pub struct BlueprintResult {
91 pub results: Vec<SpecResult>,
93
94 pub total_changes: usize,
96
97 pub modified_files: Vec<WorkspaceFilePath>,
99
100 pub success: bool,
102
103 pub error: Option<String>,
105
106 pub registry_updates: RegistryUpdateBatch,
111}
112
113#[derive(Debug, Clone)]
115pub struct SpecResult {
116 pub index: usize,
118
119 pub spec_type: String,
121
122 pub changes: usize,
124
125 pub affected_files: Vec<WorkspaceFilePath>,
127
128 pub affected_symbols: Vec<SymbolPath>,
130
131 pub success: bool,
133
134 pub error: Option<String>,
136
137 pub registry_updates: RegistryUpdateBatch,
139
140 pub events: Vec<MutationEvent>,
142}
143
144impl BlueprintResult {
145 pub fn success(results: Vec<SpecResult>, modified_files: Vec<WorkspaceFilePath>) -> Self {
146 let total_changes = results.iter().map(|r| r.changes).sum();
147 let mut all_updates = RegistryUpdateBatch::new();
149 for result in &results {
150 for update in &result.registry_updates {
151 all_updates.push(update.clone());
152 }
153 }
154 Self {
155 results,
156 total_changes,
157 modified_files,
158 success: true,
159 error: None,
160 registry_updates: all_updates,
161 }
162 }
163
164 pub fn failure(error: impl Into<String>) -> Self {
165 Self {
166 results: vec![],
167 total_changes: 0,
168 modified_files: vec![],
169 success: false,
170 error: Some(error.into()),
171 registry_updates: RegistryUpdateBatch::new(),
172 }
173 }
174}
175
176macro_rules! try_resolve {
181 ($self:expr, $target:expr, $ctx:expr, $index:expr, $spec_type:expr, $affected_symbols:expr, $msg:expr $(,)?) => {
182 match $self.resolve_target_symbol_simple($target, $ctx) {
183 Ok(id) => id,
184 Err(e) => {
185 return SpecResult {
186 index: $index,
187 spec_type: $spec_type.clone(),
188 success: false,
189 changes: 0,
190 affected_files: vec![],
191 affected_symbols: $affected_symbols.clone(),
192 error: Some(format!("{}: {}", $msg, e)),
193 registry_updates: RegistryUpdateBatch::new(),
194 events: vec![],
195 };
196 }
197 }
198 };
199}
200
201#[derive(Debug)]
203pub struct BlueprintExecutor {
204 registry: MutationRegistry,
206
207 pub strategy: ExecutionStrategy,
209
210 pub verify_after_each: bool,
212
213 pub stop_on_error: bool,
215
216 pub ignore_conflicts: bool,
219}
220
221impl Default for BlueprintExecutor {
222 fn default() -> Self {
223 Self {
224 registry: MutationRegistry::default(),
225 strategy: ExecutionStrategy::default(),
226 verify_after_each: false,
227 stop_on_error: true,
228 ignore_conflicts: true,
229 }
230 }
231}
232
233impl BlueprintExecutor {
234 pub fn new() -> Self {
235 Self::default()
236 }
237
238 fn resolve_target_symbol_simple(
243 &self,
244 target: &super::spec::MutationTargetSymbol,
245 ctx: &AnalysisContext,
246 ) -> Result<SymbolId, String> {
247 use super::spec::MutationTargetSymbol;
248
249 match target {
250 MutationTargetSymbol::ById(id) => {
251 if ctx.registry.resolve(*id).is_none() {
253 return Err(format!("Symbol {:?} not found in registry", id));
254 }
255 Ok(*id)
256 }
257 MutationTargetSymbol::ByPath(path) => {
258 ctx.registry
260 .iter()
261 .find(|(_, p)| *p == &**path)
262 .map(|(id, _)| id)
263 .ok_or_else(|| format!("Symbol at path '{}' not found", path))
264 }
265 MutationTargetSymbol::ByKindAndName(kind, name) => {
266 let normalized_name = normalize_generic_name(name);
269 let matches: Vec<_> = ctx
270 .registry
271 .iter()
272 .filter(|(_, path)| normalize_generic_name(path.name()) == normalized_name)
273 .collect();
274
275 match matches.len() {
276 0 => Err(format!("Symbol not found: kind={:?}, name={}", kind, name)),
277 1 => Ok(matches[0].0),
278 _ => Err(format!(
279 "Multiple symbols found: kind={:?}, name={} (found {} matches)",
280 kind,
281 name,
282 matches.len()
283 )),
284 }
285 }
286 MutationTargetSymbol::ByAffectedId {
287 parent_id,
288 kind,
289 name,
290 } => {
291 let parent_path = ctx
293 .registry
294 .resolve(*parent_id)
295 .ok_or_else(|| format!("Parent SymbolId {:?} not found", parent_id))?;
296
297 if let Some(child_name) = name {
298 parent_path
300 .child(child_name)
301 .map_err(|e| format!("Invalid child path: {}", e))
302 .and_then(|child_path| {
303 ctx.registry
305 .iter()
306 .find(|(_, p)| p == &&child_path)
307 .map(|(id, _)| id)
308 .ok_or_else(|| {
309 format!(
310 "Child symbol not found: parent={:?}, kind={:?}, name={}",
311 parent_id, kind, child_name
312 )
313 })
314 })
315 } else {
316 Err(format!(
318 "Anonymous child symbols not yet supported: parent={:?}, kind={:?}",
319 parent_id, kind
320 ))
321 }
322 }
323 }
324 }
325
326 #[instrument(skip(self, blueprint, ctx), fields(mutations = blueprint.mutations.len()))]
343 pub fn execute_v2(
344 &self,
345 blueprint: &ParallelBlueprint,
346 ctx: &mut AnalysisContext,
347 ) -> BlueprintResult {
348 debug!(
349 "Starting blueprint execution with {} mutations",
350 blueprint.mutations.len()
351 );
352
353 if !self.ignore_conflicts && blueprint.needs_escalation() {
355 warn!(
356 "Blueprint has {} conflicts requiring escalation",
357 blueprint.conflicts.len()
358 );
359 return BlueprintResult::failure(format!(
360 "Blueprint has {} conflicts requiring escalation",
361 blueprint.conflicts.len()
362 ));
363 }
364
365 let mut results: Vec<SpecResult> = Vec::new();
366 let mut completed: HashSet<usize> = HashSet::new();
367
368 while completed.len() < blueprint.mutations.len() {
370 let ready = blueprint
371 .deps
372 .ready_set(blueprint.mutations.len(), &completed);
373
374 if ready.is_empty() {
375 if completed.len() < blueprint.mutations.len() {
376 warn!("Dependency cycle detected");
377 return BlueprintResult::failure("Dependency cycle detected");
378 }
379 break;
380 }
381
382 debug!("Ready to execute {} specs", ready.len());
383
384 for idx in ready {
385 let spec = &blueprint.mutations[idx];
386 debug!("Executing spec {}: {:?}", idx, spec);
387 let result = self.execute_spec_v2(idx, spec, ctx);
388
389 if let Some(ref error) = result.error {
390 warn!(
391 "Spec {} completed with error: success={}, changes={}, error={}",
392 idx, result.success, result.changes, error
393 );
394 } else {
395 debug!(
396 "Spec {} completed: success={}, changes={}, events={}",
397 idx,
398 result.success,
399 result.changes,
400 result.events.len()
401 );
402 }
403
404 let success = result.success;
405 results.push(result);
406 completed.insert(idx);
407
408 if !success && self.stop_on_error {
409 let total_changes = results.iter().map(|r| r.changes).sum();
410
411 let spec_error = results
413 .last()
414 .and_then(|r| r.error.as_ref())
415 .map(|e| format!("Spec {} failed: {}", idx, e))
416 .unwrap_or_else(|| format!("Stopped at spec {} (no error message)", idx));
417
418 warn!("Stopping on error at spec {}: {}", idx, spec_error);
419
420 return BlueprintResult {
421 results,
422 total_changes,
423 modified_files: Vec::new(),
424 success: false,
425 error: Some(spec_error),
426 registry_updates: RegistryUpdateBatch::new(),
427 };
428 }
429 }
430 }
431
432 let total_changes: usize = results.iter().map(|r| r.changes).sum();
433 info!(
434 "Blueprint execution completed: {} results, {} total changes",
435 results.len(),
436 total_changes
437 );
438
439 BlueprintResult::success(results, Vec::new())
440 }
441
442 #[instrument(skip(result, ctx), fields(total_changes = result.total_changes))]
476 pub fn sync_files_and_rebuild(
477 result: &BlueprintResult,
478 ctx: &mut AnalysisContext,
479 ) -> Result<Vec<WorkspaceFilePath>, SyncError> {
480 use crate::engine::{collect_modified_symbols, RegistryGenerator};
481 use ryo_symbol::CargoMetadataProvider;
482
483 debug!(
484 "Starting sync_files_and_rebuild with {} results",
485 result.results.len()
486 );
487
488 let all_events: Vec<MutationEvent> = result
490 .results
491 .iter()
492 .flat_map(|r| r.events.clone())
493 .collect();
494
495 debug!(
496 "Collected {} mutation events from results",
497 all_events.len()
498 );
499 if all_events.is_empty() {
502 debug!("No mutation events — skipping file sync entirely");
503 return Ok(Vec::new());
504 }
505
506 let modified_symbol_ids = collect_modified_symbols(&all_events, ctx.registry());
508 debug!("Found {} modified symbols", modified_symbol_ids.len());
509 let metadata = CargoMetadataProvider::from_directory(&ctx.workspace_root)?;
518
519 let generator = RegistryGenerator::multi_file();
520 let workspace = generator.generate_affected(
521 &ctx.ast_registry,
522 ctx.registry(),
523 &modified_symbol_ids,
524 &metadata,
525 )?;
526
527 debug!(
528 "Generator produced {} crates with {} total files",
529 workspace.crates.len(),
530 workspace.total_files()
531 );
532
533 let mut modified_files = Vec::new();
537
538 for generated_crate in workspace.crates.values() {
539 debug!(
540 "Processing crate {} with {} files",
541 generated_crate.crate_name,
542 generated_crate.files.len()
543 );
544
545 use ryo_symbol::{CrateName, WorkspacePathResolver};
547 let crate_name = CrateName::new(&generated_crate.crate_name).expect(
548 "generator-emitted crate names must already satisfy CrateName validation; \
549 reaching this expect means the generator is producing invalid names",
550 );
551 let layout = metadata.crate_layout(&crate_name);
552
553 for (crate_relative_path, generated_file) in &generated_crate.files {
554 let workspace_relative = match &layout {
556 Some(layout) => layout.to_workspace_relative(crate_relative_path),
557 None => {
558 std::path::PathBuf::from(crate_relative_path.as_str())
560 }
561 };
562
563 let workspace_file = ctx
566 .files()
567 .keys()
568 .find(|wfp| {
569 wfp.crate_name().as_str() == generated_crate.crate_name
570 && wfp.as_relative() == workspace_relative.as_path()
571 })
572 .cloned();
573
574 let wfp = if let Some(existing) = workspace_file {
575 debug!(
576 "Updating existing file: {} ({})",
577 crate_relative_path,
578 existing.as_relative().display()
579 );
580 existing
581 } else {
582 debug!(
584 "Creating new file: {} -> {} for crate {}",
585 crate_relative_path,
586 workspace_relative.display(),
587 generated_crate.crate_name
588 );
589 let resolver =
590 WorkspacePathResolver::new(ctx.workspace_root.as_ref().to_path_buf());
591 resolver.resolve_relative_with_crate(&workspace_relative, crate_name.clone())
592 };
593
594 let parsed = ryo_source::pure::PureFile::from_source(&generated_file.source);
595 let pure_file = parsed.unwrap_or_else(|_e| ryo_source::pure::PureFile::new());
596 ctx.files_mut().insert(wfp.clone(), Arc::new(pure_file));
597 modified_files.push(wfp);
598 }
599 }
600
601 debug!(
602 "Updated {} files in context from generator output",
603 modified_files.len()
604 );
605
606 if !all_events.is_empty() {
608 let affected_ids = collect_affected_ids(&all_events, ctx.registry());
609 debug!("Rebuilding with {} affected symbol IDs", affected_ids.len());
610 ctx.rebuild_after_mutation_by_symbols(&affected_ids);
611 } else if !modified_files.is_empty() {
612 debug!(
614 "Rebuilding with {} modified files (fallback)",
615 modified_files.len()
616 );
617 ctx.rebuild_after_mutation(&modified_files);
618 }
619
620 info!(
621 "sync_files_and_rebuild completed: {} files modified",
622 modified_files.len()
623 );
624 Ok(modified_files)
625 }
626
627 fn execute_spec_v2(
632 &self,
633 index: usize,
634 spec: &MutationSpec,
635 ctx: &mut AnalysisContext,
636 ) -> SpecResult {
637 use crate::engine::{ASTMutationEngine, ExecutionResult};
638 use crate::executor::registry::ConvertError;
639 use ryo_mutations::basic::{
640 AddDeriveMutation, AddFieldMutation, RemoveDeriveMutation, RemoveFieldMutation,
641 RemoveModMutation,
642 };
643
644 let spec_type = spec_type_name(spec);
645 let affected_symbols: Vec<SymbolPath> = spec
647 .get_targets()
648 .iter()
649 .filter_map(|target| target.to_path(ctx.registry()))
650 .collect();
651
652 match self.registry.convert_v2(spec, ctx) {
654 Ok(mutations) => {
655 let exec_result = ASTMutationEngine::execute_ast_reg_batch_dyn(mutations, ctx);
656
657 if let MutationSpec::AddItem {
660 target, content, ..
661 } = spec
662 {
663 if let MutationTargetSymbol::ByPath(path) = target {
665 register_item_from_content(ctx, path, content);
666 } else if let MutationTargetSymbol::ById(id) = target {
667 if let Some(path) = ctx.registry().resolve(*id).cloned() {
669 register_item_from_content(ctx, &path, content);
670 }
671 }
672 }
673
674 return SpecResult {
675 index,
676 spec_type,
677 success: true,
678 changes: exec_result.result.changes,
679 affected_files: vec![], affected_symbols,
681 error: None,
682 registry_updates: RegistryUpdateBatch::new(),
683 events: exec_result.events,
684 };
685 }
686 Err(ConvertError::V2NotSupported) => {
687 }
689 Err(e) => {
690 return SpecResult {
692 index,
693 spec_type,
694 success: false,
695 changes: 0,
696 affected_files: vec![],
697 affected_symbols,
698 error: Some(format!("convert_v2 failed: {}", e)),
699 registry_updates: RegistryUpdateBatch::new(),
700 events: vec![],
701 };
702 }
703 }
704
705 let exec_result = match spec {
707 MutationSpec::AddField {
708 target,
709 field_name,
710 field_type,
711 visibility,
712 ..
713 } => {
714 let symbol_id = try_resolve!(
716 self,
717 target,
718 ctx,
719 index,
720 spec_type,
721 affected_symbols,
722 "Failed to resolve target symbol"
723 );
724 let mut mutation = AddFieldMutation::new(symbol_id, field_name, field_type);
725 if matches!(visibility, super::spec::Visibility::Pub) {
726 mutation = mutation.public();
727 }
728 ASTMutationEngine::execute_ast_reg(&mutation, ctx)
729 }
730
731 MutationSpec::RemoveField {
732 target, field_name, ..
733 } => {
734 let symbol_id = try_resolve!(
736 self,
737 target,
738 ctx,
739 index,
740 spec_type,
741 affected_symbols,
742 "Failed to resolve target symbol"
743 );
744 let mutation = RemoveFieldMutation::new(symbol_id, field_name);
745 ASTMutationEngine::execute_ast_reg(&mutation, ctx)
746 }
747
748 MutationSpec::RemoveMod {
749 target, mod_name, ..
750 } => {
751 use ryo_symbol::SymbolKind;
752
753 let parent_id = try_resolve!(
755 self,
756 target,
757 ctx,
758 index,
759 spec_type,
760 affected_symbols,
761 "Failed to resolve parent module"
762 );
763
764 let parent_path = match ctx.registry.resolve(parent_id) {
766 Some(p) => p,
767 None => {
768 return SpecResult {
769 index,
770 spec_type: spec_type.clone(),
771 success: false,
772 changes: 0,
773 affected_files: vec![],
774 affected_symbols: affected_symbols.clone(),
775 error: Some(format!(
776 "Parent module path not found for SymbolId {:?}",
777 parent_id
778 )),
779 registry_updates: RegistryUpdateBatch::new(),
780 events: vec![],
781 };
782 }
783 };
784
785 let mod_path = match parent_path.child(mod_name) {
786 Ok(p) => p,
787 Err(e) => {
788 return SpecResult {
789 index,
790 spec_type: spec_type.clone(),
791 success: false,
792 changes: 0,
793 affected_files: vec![],
794 affected_symbols: affected_symbols.clone(),
795 error: Some(format!(
796 "Failed to build module path for '{}': {}",
797 mod_name, e
798 )),
799 registry_updates: RegistryUpdateBatch::new(),
800 events: vec![],
801 };
802 }
803 };
804
805 let module_id = match ctx.registry.lookup(&mod_path) {
806 Some(id) => id,
807 None => {
808 return SpecResult {
809 index,
810 spec_type: spec_type.clone(),
811 success: false,
812 changes: 0,
813 affected_files: vec![],
814 affected_symbols: affected_symbols.clone(),
815 error: Some(format!(
816 "Module '{}' not found in {}",
817 mod_name, parent_path
818 )),
819 registry_updates: RegistryUpdateBatch::new(),
820 events: vec![],
821 };
822 }
823 };
824
825 if ctx.registry.kind(module_id) != Some(SymbolKind::Mod) {
827 return SpecResult {
828 index,
829 spec_type: spec_type.clone(),
830 success: false,
831 changes: 0,
832 affected_files: vec![],
833 affected_symbols: affected_symbols.clone(),
834 error: Some(format!("Symbol {} is not a module", module_id)),
835 registry_updates: RegistryUpdateBatch::new(),
836 events: vec![],
837 };
838 }
839
840 let mutation = RemoveModMutation::new(module_id);
841 ASTMutationEngine::execute_ast_reg(&mutation, ctx)
842 }
843
844 MutationSpec::AddDerive {
845 target, derives, ..
846 } => {
847 let symbol_id = try_resolve!(
849 self,
850 target,
851 ctx,
852 index,
853 spec_type,
854 affected_symbols,
855 "Failed to resolve target symbol"
856 );
857 let mutation = AddDeriveMutation::new(symbol_id, derives.clone());
858 ASTMutationEngine::execute_ast_reg(&mutation, ctx)
859 }
860
861 MutationSpec::RemoveDerive {
862 target, derives, ..
863 } => {
864 let symbol_id = try_resolve!(
866 self,
867 target,
868 ctx,
869 index,
870 spec_type,
871 affected_symbols,
872 "Failed to resolve target symbol"
873 );
874 let mutation = RemoveDeriveMutation::new(symbol_id, derives.clone());
875 ASTMutationEngine::execute_ast_reg(&mutation, ctx)
876 }
877
878 MutationSpec::AddVariant {
879 target,
880 variant_name,
881 variant_kind,
882 ..
883 } => {
884 use ryo_mutations::basic::AddVariantMutation;
885 use ryo_source::pure::{PureField, PureFields, PureType, PureVis};
886
887 let symbol_id = try_resolve!(
889 self,
890 target,
891 ctx,
892 index,
893 spec_type,
894 affected_symbols,
895 "Failed to resolve target symbol"
896 );
897
898 let fields = match variant_kind {
899 super::spec::VariantKind::Unit => PureFields::Unit,
900 super::spec::VariantKind::Tuple { types } => {
901 PureFields::Tuple(types.iter().map(|t| PureType::Path(t.clone())).collect())
902 }
903 super::spec::VariantKind::Struct { fields } => {
904 let pure_fields: Vec<PureField> = fields
905 .iter()
906 .map(|(n, t)| PureField {
907 attrs: Vec::new(),
908 vis: PureVis::Private,
909 name: n.clone(),
910 ty: PureType::Path(t.clone()),
911 })
912 .collect();
913 PureFields::Named(pure_fields)
914 }
915 };
916 let mutation = AddVariantMutation::new(symbol_id, variant_name, fields);
917 ASTMutationEngine::execute_ast_reg(&mutation, ctx)
918 }
919
920 MutationSpec::RemoveVariant {
921 target,
922 variant_name,
923 ..
924 } => {
925 use ryo_mutations::basic::RemoveVariantMutation;
926 let symbol_id = try_resolve!(
928 self,
929 target,
930 ctx,
931 index,
932 spec_type,
933 affected_symbols,
934 "Failed to resolve target symbol"
935 );
936 let mutation = RemoveVariantMutation::new(symbol_id, variant_name);
937 ASTMutationEngine::execute_ast_reg(&mutation, ctx)
938 }
939
940 MutationSpec::ChangeVisibility {
941 target, visibility, ..
942 } => {
943 use ryo_mutations::basic::ChangeVisibilityMutation;
944 use ryo_source::pure::PureVis;
945
946 let symbol_id = try_resolve!(
948 self,
949 target,
950 ctx,
951 index,
952 spec_type,
953 affected_symbols,
954 "Failed to resolve target symbol"
955 );
956
957 let pure_vis = match visibility {
958 super::spec::Visibility::Private => PureVis::Private,
959 super::spec::Visibility::Pub => PureVis::Public,
960 super::spec::Visibility::PubCrate => PureVis::Crate,
961 super::spec::Visibility::PubSuper => PureVis::Super,
962 super::spec::Visibility::PubIn(_) => PureVis::Public, };
964
965 let mutation = ChangeVisibilityMutation::new(symbol_id, pure_vis);
966 ASTMutationEngine::execute_ast_reg(&mutation, ctx)
967 }
968
969 MutationSpec::Rename { target, to, .. } => {
970 use ryo_mutations::basic::RenameMutation;
971 let symbol_id = try_resolve!(
973 self,
974 target,
975 ctx,
976 index,
977 spec_type,
978 affected_symbols,
979 "Failed to resolve target symbol"
980 );
981 let mutation = RenameMutation::new(symbol_id, to);
982 ASTMutationEngine::execute_ast_reg(&mutation, ctx)
983 }
984
985 MutationSpec::AddMatchArm {
988 target,
989 enum_name,
990 pattern,
991 body,
992 } => {
993 use ryo_mutations::basic::AddMatchArmMutation;
994
995 let fn_id = match self.resolve_target_symbol_simple(target, ctx) {
996 Ok(id) => id,
997 Err(e) => {
998 return SpecResult {
999 index,
1000 spec_type,
1001 changes: 0,
1002 affected_files: vec![],
1003 affected_symbols: vec![],
1004 success: false,
1005 error: Some(e),
1006 registry_updates: RegistryUpdateBatch::default(),
1007 events: vec![],
1008 };
1009 }
1010 };
1011
1012 let mutation = AddMatchArmMutation::new(fn_id, enum_name, pattern, body);
1013 ASTMutationEngine::execute_ast_reg(&mutation, ctx)
1014 }
1015
1016 MutationSpec::RemoveMatchArm {
1017 target,
1018 enum_name,
1019 pattern,
1020 } => {
1021 use ryo_mutations::basic::RemoveMatchArmMutation;
1022
1023 let fn_id = match self.resolve_target_symbol_simple(target, ctx) {
1024 Ok(id) => id,
1025 Err(e) => {
1026 return SpecResult {
1027 index,
1028 spec_type,
1029 changes: 0,
1030 affected_files: vec![],
1031 affected_symbols: vec![],
1032 success: false,
1033 error: Some(e),
1034 registry_updates: RegistryUpdateBatch::default(),
1035 events: vec![],
1036 };
1037 }
1038 };
1039
1040 let mutation = RemoveMatchArmMutation::new(fn_id, enum_name, pattern);
1041 ASTMutationEngine::execute_ast_reg(&mutation, ctx)
1042 }
1043
1044 MutationSpec::ReplaceMatchArm {
1045 target,
1046 enum_name,
1047 old_pattern,
1048 new_pattern,
1049 new_body,
1050 } => {
1051 use ryo_mutations::basic::ReplaceMatchArmMutation;
1052
1053 let fn_id = match self.resolve_target_symbol_simple(target, ctx) {
1054 Ok(id) => id,
1055 Err(e) => {
1056 return SpecResult {
1057 index,
1058 spec_type,
1059 changes: 0,
1060 affected_files: vec![],
1061 affected_symbols: vec![],
1062 success: false,
1063 error: Some(e),
1064 registry_updates: RegistryUpdateBatch::default(),
1065 events: vec![],
1066 };
1067 }
1068 };
1069
1070 let mutation = ReplaceMatchArmMutation::new(
1071 fn_id,
1072 enum_name,
1073 old_pattern,
1074 new_pattern,
1075 new_body,
1076 );
1077 ASTMutationEngine::execute_ast_reg(&mutation, ctx)
1078 }
1079
1080 MutationSpec::AddStructLiteralField {
1081 target,
1082 field_name,
1083 value,
1084 ..
1085 } => {
1086 use ryo_mutations::basic::AddStructLiteralFieldMutation;
1087 let symbol_id = try_resolve!(
1089 self,
1090 target,
1091 ctx,
1092 index,
1093 spec_type,
1094 affected_symbols,
1095 "Failed to resolve target symbol"
1096 );
1097 let mutation = AddStructLiteralFieldMutation::new(symbol_id, field_name, value);
1098 ASTMutationEngine::execute_ast_reg(&mutation, ctx)
1099 }
1100
1101 MutationSpec::RemoveStructLiteralField {
1102 target, field_name, ..
1103 } => {
1104 use ryo_mutations::basic::RemoveStructLiteralFieldMutation;
1105 let symbol_id = try_resolve!(
1107 self,
1108 target,
1109 ctx,
1110 index,
1111 spec_type,
1112 affected_symbols,
1113 "Failed to resolve target symbol"
1114 );
1115 let mutation = RemoveStructLiteralFieldMutation::new(symbol_id, field_name);
1116 ASTMutationEngine::execute_ast_reg(&mutation, ctx)
1117 }
1118
1119 MutationSpec::OrganizeImports {
1120 deduplicate,
1121 merge_groups,
1122 ..
1123 } => {
1124 use ryo_mutations::idiom::OrganizeImportsMutation;
1125 let mutation = OrganizeImportsMutation::new()
1126 .with_deduplicate(*deduplicate)
1127 .with_merge_groups(*merge_groups);
1128 ASTMutationEngine::execute_ast_reg(&mutation, ctx)
1129 }
1130
1131 MutationSpec::AssignOp { .. } => {
1132 use ryo_mutations::idiom::AssignOpMutation;
1133 let mutation = AssignOpMutation::new();
1134 ASTMutationEngine::execute_ast_reg(&mutation, ctx)
1135 }
1136
1137 MutationSpec::BoolSimplify { .. } => {
1138 use ryo_mutations::idiom::BoolSimplifyMutation;
1139 let mutation = BoolSimplifyMutation::new();
1140 ASTMutationEngine::execute_ast_reg(&mutation, ctx)
1141 }
1142
1143 MutationSpec::ComparisonToMethod { .. } => {
1144 use ryo_mutations::idiom::ComparisonToMethodMutation;
1145 let mutation = ComparisonToMethodMutation::new();
1146 ASTMutationEngine::execute_ast_reg(&mutation, ctx)
1147 }
1148
1149 MutationSpec::CollapsibleIf { .. } => {
1150 use ryo_mutations::idiom::CollapsibleIfMutation;
1151 let mutation = CollapsibleIfMutation::new();
1152 ASTMutationEngine::execute_ast_reg(&mutation, ctx)
1153 }
1154
1155 MutationSpec::RedundantClosure { .. } => {
1156 use ryo_mutations::idiom::RedundantClosureMutation;
1157 let mutation = RedundantClosureMutation::new();
1158 ASTMutationEngine::execute_ast_reg(&mutation, ctx)
1159 }
1160
1161 MutationSpec::FilterNext { .. } => {
1162 use ryo_mutations::idiom::FilterNextMutation;
1163 let mutation = FilterNextMutation::new();
1164 ASTMutationEngine::execute_ast_reg(&mutation, ctx)
1165 }
1166
1167 MutationSpec::MapUnwrapOr { .. } => {
1168 use ryo_mutations::idiom::MapUnwrapOrMutation;
1169 let mutation = MapUnwrapOrMutation::new();
1170 ASTMutationEngine::execute_ast_reg(&mutation, ctx)
1171 }
1172
1173 MutationSpec::CloneOnCopy { .. } => {
1174 use ryo_mutations::idiom::CloneOnCopyMutation;
1175 let mutation = CloneOnCopyMutation::new();
1176 ASTMutationEngine::execute_ast_reg(&mutation, ctx)
1177 }
1178
1179 MutationSpec::LoopToIterator { .. } => {
1180 use ryo_mutations::idiom::LoopToIteratorMutation;
1181 let mutation = LoopToIteratorMutation::new();
1182 ASTMutationEngine::execute_ast_reg(&mutation, ctx)
1183 }
1184
1185 MutationSpec::UnwrapToQuestion { .. } => {
1186 use ryo_mutations::idiom::UnwrapToQuestionMutation;
1187 let mutation = UnwrapToQuestionMutation::new();
1188 ASTMutationEngine::execute_ast_reg(&mutation, ctx)
1189 }
1190
1191 MutationSpec::ManualMap { .. } => {
1192 use ryo_mutations::idiom::ManualMapMutation;
1193 let mutation = ManualMapMutation::new();
1194 ASTMutationEngine::execute_ast_reg(&mutation, ctx)
1195 }
1196
1197 MutationSpec::MatchToIfLet { .. } => {
1198 use ryo_mutations::idiom::MatchToIfLetMutation;
1199 let mutation = MatchToIfLetMutation::new();
1200 ASTMutationEngine::execute_ast_reg(&mutation, ctx)
1201 }
1202
1203 MutationSpec::IntroduceVariable { expr, var_name, .. } => {
1206 use ryo_mutations::idiom::IntroduceVariableMutation;
1207 use ryo_source::ToPure;
1208 let pure_expr = syn::parse_str::<syn::Expr>(expr)
1210 .map(|e| e.to_pure())
1211 .unwrap_or_else(|_| ryo_source::pure::PureExpr::Path(expr.clone()));
1212 let mutation = IntroduceVariableMutation::new(pure_expr, var_name);
1213 ASTMutationEngine::execute_ast_reg(&mutation, ctx)
1214 }
1215
1216 MutationSpec::ExtractTrait {
1217 target,
1218 ref trait_name,
1219 ref methods,
1220 ..
1221 } => {
1222 use ryo_mutations::basic::ExtractTraitMutation;
1223 let symbol_id = try_resolve!(
1225 self,
1226 target,
1227 ctx,
1228 index,
1229 spec_type,
1230 affected_symbols,
1231 "Failed to resolve target symbol"
1232 );
1233 let mut mutation = ExtractTraitMutation::new(symbol_id, trait_name.clone());
1234 if let Some(ref m) = methods {
1235 mutation = mutation.with_methods(m.clone());
1236 }
1237 ASTMutationEngine::execute_ast_reg(&mutation, ctx)
1238 }
1239
1240 MutationSpec::InlineTrait {
1241 target,
1242 ref struct_name,
1243 remove_trait,
1244 ..
1245 } => {
1246 use ryo_mutations::basic::InlineTraitMutation;
1247 let symbol_id = try_resolve!(
1249 self,
1250 target,
1251 ctx,
1252 index,
1253 spec_type,
1254 affected_symbols,
1255 "Failed to resolve target symbol"
1256 );
1257 let mut mutation = InlineTraitMutation::new(symbol_id, struct_name.clone());
1258 if !remove_trait {
1259 mutation = mutation.keep_trait();
1260 }
1261 ASTMutationEngine::execute_ast_reg(&mutation, ctx)
1262 }
1263
1264 MutationSpec::ReplaceExpr {
1265 fn_id,
1266 old_expr,
1267 new_expr,
1268 replace_all,
1269 ..
1270 } => {
1271 use ryo_mutations::basic::stmt::ReplaceExprMutation;
1272 use ryo_source::pure::{PureExpr, ToPure};
1273
1274 let target_fn = match fn_id {
1276 Some(id) => *id,
1277 None => {
1278 return SpecResult {
1279 index,
1280 spec_type,
1281 changes: 0,
1282 affected_files: vec![],
1283 affected_symbols: vec![],
1284 success: false,
1285 error: Some(
1286 "ReplaceExpr requires fn_id in V1 executor. Use V2 converter for all-functions support.".to_string()
1287 ),
1288 registry_updates: RegistryUpdateBatch::default(),
1289 events: vec![],
1290 };
1291 }
1292 };
1293
1294 let old_pure = syn::parse_str::<syn::Expr>(old_expr)
1296 .map(|e| e.to_pure())
1297 .unwrap_or_else(|_| PureExpr::Path(old_expr.clone()));
1298 let new_pure = syn::parse_str::<syn::Expr>(new_expr)
1299 .map(|e| e.to_pure())
1300 .unwrap_or_else(|_| PureExpr::Path(new_expr.clone()));
1301
1302 let mut mutation = ReplaceExprMutation::new(old_pure, new_pure, target_fn);
1303 if !replace_all {
1304 mutation = mutation.first_only();
1305 }
1306 ASTMutationEngine::execute_ast_reg(&mutation, ctx)
1307 }
1308
1309 MutationSpec::RemoveStatement {
1310 fn_id,
1311 ref pattern,
1312 remove_all,
1313 ..
1314 } => {
1315 use crate::executor::registry::converters::StmtConverter;
1316 use ryo_mutations::basic::stmt::RemoveStatementMutation;
1317
1318 let target_fn = match fn_id {
1320 Some(id) => *id,
1321 None => {
1322 return SpecResult {
1323 index,
1324 spec_type,
1325 changes: 0,
1326 affected_files: vec![],
1327 affected_symbols: vec![],
1328 success: false,
1329 error: Some(
1330 "RemoveStatement requires fn_id in V1 executor. Use V2 converter for all-functions support.".to_string()
1331 ),
1332 registry_updates: RegistryUpdateBatch::default(),
1333 events: vec![],
1334 };
1335 }
1336 };
1337
1338 let target_stmt = match StmtConverter::parse_stmt(pattern) {
1339 Ok(s) => s,
1340 Err(e) => {
1341 return SpecResult {
1342 index,
1343 spec_type,
1344 changes: 0,
1345 affected_files: vec![],
1346 affected_symbols: vec![],
1347 success: false,
1348 error: Some(format!("Failed to parse statement pattern: {}", e)),
1349 registry_updates: RegistryUpdateBatch::default(),
1350 events: vec![],
1351 };
1352 }
1353 };
1354
1355 let mut mutation =
1356 RemoveStatementMutation::new(target_stmt, pattern.clone(), target_fn);
1357 if !*remove_all {
1358 mutation = mutation.first_only();
1359 }
1360 ASTMutationEngine::execute_ast_reg(&mutation, ctx)
1361 }
1362
1363 MutationSpec::InsertStatement {
1364 fn_id,
1365 ref stmt,
1366 ref position,
1367 ref reference_pattern,
1368 ..
1369 } => {
1370 use crate::executor::registry::converters::StmtConverter;
1371 use ryo_mutations::basic::stmt::InsertStatementMutation;
1372
1373 let pure_stmt = match StmtConverter::parse_stmt(stmt) {
1374 Ok(s) => s,
1375 Err(e) => {
1376 return SpecResult {
1377 index,
1378 spec_type,
1379 changes: 0,
1380 affected_files: vec![],
1381 affected_symbols: vec![],
1382 success: false,
1383 error: Some(format!("Failed to parse statement: {}", e)),
1384 registry_updates: RegistryUpdateBatch::default(),
1385 events: vec![],
1386 };
1387 }
1388 };
1389
1390 let mut mutation = InsertStatementMutation::new(pure_stmt, *fn_id);
1391 mutation = match position {
1392 StmtInsertPosition::Start => mutation.at_start(),
1393 StmtInsertPosition::End => mutation.at_end(),
1394 StmtInsertPosition::BeforePattern => {
1395 if let Some(ref p) = reference_pattern {
1396 let reference_stmt = match StmtConverter::parse_stmt(p) {
1397 Ok(s) => s,
1398 Err(e) => {
1399 return SpecResult {
1400 index,
1401 spec_type,
1402 changes: 0,
1403 affected_files: vec![],
1404 affected_symbols: vec![],
1405 success: false,
1406 error: Some(format!(
1407 "Failed to parse reference pattern: {}",
1408 e
1409 )),
1410 registry_updates: RegistryUpdateBatch::default(),
1411 events: vec![],
1412 };
1413 }
1414 };
1415 mutation.before(reference_stmt)
1416 } else {
1417 mutation
1418 }
1419 }
1420 StmtInsertPosition::AfterPattern => {
1421 if let Some(ref p) = reference_pattern {
1422 let reference_stmt = match StmtConverter::parse_stmt(p) {
1423 Ok(s) => s,
1424 Err(e) => {
1425 return SpecResult {
1426 index,
1427 spec_type,
1428 changes: 0,
1429 affected_files: vec![],
1430 affected_symbols: vec![],
1431 success: false,
1432 error: Some(format!(
1433 "Failed to parse reference pattern: {}",
1434 e
1435 )),
1436 registry_updates: RegistryUpdateBatch::default(),
1437 events: vec![],
1438 };
1439 }
1440 };
1441 mutation.after(reference_stmt)
1442 } else {
1443 mutation
1444 }
1445 }
1446 };
1447 ASTMutationEngine::execute_ast_reg(&mutation, ctx)
1448 }
1449
1450 MutationSpec::ReplaceStatement {
1451 old_stmt,
1452 new_stmt,
1453 fn_id,
1454 ..
1455 } => {
1456 use crate::executor::registry::converters::StmtConverter;
1457 use ryo_mutations::basic::stmt::ReplaceStatementMutation;
1458
1459 let old_pure = match StmtConverter::parse_stmt(old_stmt) {
1460 Ok(s) => s,
1461 Err(e) => {
1462 return SpecResult {
1463 index,
1464 spec_type,
1465 changes: 0,
1466 affected_files: vec![],
1467 affected_symbols: vec![],
1468 success: false,
1469 error: Some(format!("Failed to parse old statement: {}", e)),
1470 registry_updates: RegistryUpdateBatch::default(),
1471 events: vec![],
1472 };
1473 }
1474 };
1475 let new_pure = match StmtConverter::parse_stmt(new_stmt) {
1476 Ok(s) => s,
1477 Err(e) => {
1478 return SpecResult {
1479 index,
1480 spec_type,
1481 changes: 0,
1482 affected_files: vec![],
1483 affected_symbols: vec![],
1484 success: false,
1485 error: Some(format!("Failed to parse new statement: {}", e)),
1486 registry_updates: RegistryUpdateBatch::default(),
1487 events: vec![],
1488 };
1489 }
1490 };
1491
1492 let target_fn = match fn_id {
1494 Some(id) => *id,
1495 None => {
1496 return SpecResult {
1497 index,
1498 spec_type,
1499 changes: 0,
1500 affected_files: vec![],
1501 affected_symbols: vec![],
1502 success: false,
1503 error: Some(
1504 "ReplaceStatement requires fn_id in V1 executor. Use V2 converter for all-functions support.".to_string()
1505 ),
1506 registry_updates: RegistryUpdateBatch::default(),
1507 events: vec![],
1508 };
1509 }
1510 };
1511
1512 let mutation = ReplaceStatementMutation::new(old_pure, new_pure, target_fn);
1513 ASTMutationEngine::execute_ast_reg(&mutation, ctx)
1514 }
1515
1516 MutationSpec::DuplicateFunction { .. }
1517 | MutationSpec::DuplicateStruct { .. }
1518 | MutationSpec::DuplicateEnum { .. }
1519 | MutationSpec::DuplicateModTree { .. } => {
1520 return SpecResult {
1526 index,
1527 spec_type,
1528 success: false,
1529 changes: 0,
1530 affected_files: vec![],
1531 affected_symbols,
1532 error: Some(
1533 "Duplicate mutations must be routed through \
1534 DuplicateConverter::convert_v2(); the V1 executor path \
1535 has been removed."
1536 .to_string(),
1537 ),
1538 registry_updates: RegistryUpdateBatch::new(),
1539 events: vec![],
1540 };
1541 }
1542
1543 MutationSpec::AddSpec { .. } => {
1545 ExecutionResult::new(
1547 MutationResult {
1548 mutation_type: "AddSpec".to_string(),
1549 changes: 0,
1550 description: "V2 pending - implement as AddTypeAlias composition"
1551 .to_string(),
1552 },
1553 vec![],
1554 )
1555 }
1556
1557 MutationSpec::PluginTransform { .. } => {
1559 ExecutionResult::new(
1561 MutationResult {
1562 mutation_type: "PluginTransform".to_string(),
1563 changes: 0,
1564 description: "WASM plugin runtime not implemented in V2".to_string(),
1565 },
1566 vec![],
1567 )
1568 }
1569
1570 _ => {
1571 return SpecResult {
1572 index,
1573 spec_type: spec_type.clone(),
1574 success: false,
1575 changes: 0,
1576 affected_files: vec![],
1577 affected_symbols,
1578 error: Some(format!(
1579 "MutationSpec::{} is not implemented in the V2 AST path \
1580 (no converter or fallback covers this variant). \
1581 If you need this spec, add a converter to \
1582 MutationRegistry::convert_v2 or extend execute_spec_v2.",
1583 spec_type
1584 )),
1585 registry_updates: RegistryUpdateBatch::new(),
1586 events: vec![],
1587 };
1588 }
1589 };
1590
1591 SpecResult {
1592 index,
1593 spec_type,
1594 changes: exec_result.result.changes,
1595 affected_files: vec![], affected_symbols,
1597 success: exec_result.has_changes() || exec_result.result.changes == 0,
1598 error: None,
1599 registry_updates: RegistryUpdateBatch::new(),
1600 events: exec_result.events,
1601 }
1602 }
1603
1604 pub fn with_strategy(mut self, strategy: ExecutionStrategy) -> Self {
1606 self.strategy = strategy;
1607 self
1608 }
1609
1610 pub fn with_verify(mut self, verify: bool) -> Self {
1612 self.verify_after_each = verify;
1613 self
1614 }
1615
1616 pub fn with_stop_on_error(mut self, stop: bool) -> Self {
1618 self.stop_on_error = stop;
1619 self
1620 }
1621}
1622
1623fn normalize_generic_name(name: &str) -> String {
1629 name.replace(" < ", "<")
1630 .replace(" > ", ">")
1631 .replace("< ", "<")
1632 .replace(" >", ">")
1633 .replace(", ", ",")
1634 .replace(" ,", ",")
1635}
1636
1637fn register_item_from_content(
1642 ctx: &mut AnalysisContext,
1643 target: &ryo_symbol::SymbolPath,
1644 content: &str,
1645) {
1646 use ryo_symbol::SymbolKind;
1647
1648 let trimmed = content.trim();
1649
1650 let mut lines = trimmed.lines();
1652 let mut decl_line = "";
1653 for line in lines.by_ref() {
1654 let line = line.trim();
1655 if !line.starts_with('#') && !line.starts_with("//") && !line.is_empty() {
1656 decl_line = line;
1657 break;
1658 }
1659 }
1660
1661 let tokens: Vec<&str> = decl_line.split_whitespace().collect();
1663 if tokens.is_empty() {
1664 return;
1665 }
1666
1667 let mut idx = 0;
1668
1669 if tokens.get(idx) == Some(&"pub") {
1671 idx += 1;
1672 if let Some(t) = tokens.get(idx) {
1674 if t.starts_with('(') {
1675 idx += 1;
1676 }
1677 }
1678 }
1679
1680 let Some(keyword) = tokens.get(idx) else {
1682 return;
1683 };
1684 idx += 1;
1685
1686 let kind = match *keyword {
1687 "struct" => SymbolKind::Struct,
1688 "enum" => SymbolKind::Enum,
1689 "fn" => SymbolKind::Function,
1690 "type" => SymbolKind::TypeAlias,
1691 "const" => SymbolKind::Const,
1692 "static" => SymbolKind::Static,
1693 "trait" => SymbolKind::Trait,
1694 "mod" => SymbolKind::Mod,
1695 "impl" => return, _ => return,
1697 };
1698
1699 let Some(name_token) = tokens.get(idx) else {
1701 return;
1702 };
1703 let name = name_token
1704 .split(['<', '{', '(', ':'])
1705 .next()
1706 .unwrap_or(name_token);
1707
1708 let target_str = target.to_string();
1710 let is_crate_root = target_str == "crate";
1711 let full_path = if is_crate_root {
1712 ryo_symbol::SymbolPath::parse(&format!("crate::{}", name))
1713 } else {
1714 ryo_symbol::SymbolPath::parse(&format!("{}::{}", target_str, name))
1715 };
1716
1717 let Ok(path) = full_path else {
1718 return;
1719 };
1720
1721 let _ = ctx.registry_mut().register(path, kind);
1723}
1724
1725fn spec_type_name(spec: &MutationSpec) -> String {
1727 match spec {
1728 MutationSpec::Rename { .. } => "Rename".to_string(),
1729 MutationSpec::AddField { .. } => "AddField".to_string(),
1730 MutationSpec::RemoveField { .. } => "RemoveField".to_string(),
1731 MutationSpec::ChangeVisibility { .. } => "ChangeVisibility".to_string(),
1732 MutationSpec::AddDerive { .. } => "AddDerive".to_string(),
1733 MutationSpec::RemoveDerive { .. } => "RemoveDerive".to_string(),
1734 MutationSpec::AddVariant { .. } => "AddVariant".to_string(),
1735 MutationSpec::RemoveVariant { .. } => "RemoveVariant".to_string(),
1736 MutationSpec::AddMatchArm { .. } => "AddMatchArm".to_string(),
1737 MutationSpec::RemoveMatchArm { .. } => "RemoveMatchArm".to_string(),
1738 MutationSpec::ReplaceMatchArm { .. } => "ReplaceMatchArm".to_string(),
1739 MutationSpec::AddStructLiteralField { .. } => "AddStructLiteralField".to_string(),
1740 MutationSpec::RemoveStructLiteralField { .. } => "RemoveStructLiteralField".to_string(),
1741 MutationSpec::AddItem { .. } => "AddItem".to_string(),
1742 MutationSpec::RemoveItem { .. } => "RemoveItem".to_string(),
1743 MutationSpec::AddMethod { .. } => "AddMethod".to_string(),
1744 MutationSpec::RemoveMethod { .. } => "RemoveMethod".to_string(),
1745 MutationSpec::RemoveMod { .. } => "RemoveMod".to_string(),
1746 MutationSpec::CreateMod { .. } => "CreateMod".to_string(),
1747 MutationSpec::OrganizeImports { .. } => "OrganizeImports".to_string(),
1748 MutationSpec::LoopToIterator { .. } => "LoopToIterator".to_string(),
1749 MutationSpec::UnwrapToQuestion { .. } => "UnwrapToQuestion".to_string(),
1750 MutationSpec::AddSpec { .. } => "AddSpec".to_string(),
1751 MutationSpec::RemoveSpec { .. } => "RemoveSpec".to_string(),
1752 MutationSpec::ValidateSpec { .. } => "ValidateSpec".to_string(),
1753 MutationSpec::ExtractTrait { .. } => "ExtractTrait".to_string(),
1754 MutationSpec::InlineTrait { .. } => "InlineTrait".to_string(),
1755 MutationSpec::ReplaceType { .. } => "ReplaceType".to_string(),
1756 MutationSpec::EnumToTrait { .. } => "EnumToTrait".to_string(),
1757 MutationSpec::MoveItem { .. } => "MoveItem".to_string(),
1758 MutationSpec::AssignOp { .. } => "AssignOp".to_string(),
1759 MutationSpec::BoolSimplify { .. } => "BoolSimplify".to_string(),
1760 MutationSpec::CloneOnCopy { .. } => "CloneOnCopy".to_string(),
1761 MutationSpec::CollapsibleIf { .. } => "CollapsibleIf".to_string(),
1762 MutationSpec::NoOpArmToTodo { .. } => "NoOpArmToTodo".to_string(),
1763 MutationSpec::ComparisonToMethod { .. } => "ComparisonToMethod".to_string(),
1764 MutationSpec::RedundantClosure { .. } => "RedundantClosure".to_string(),
1765 MutationSpec::IntroduceVariable { .. } => "IntroduceVariable".to_string(),
1766 MutationSpec::ManualMap { .. } => "ManualMap".to_string(),
1767 MutationSpec::MatchToIfLet { .. } => "MatchToIfLet".to_string(),
1768 MutationSpec::FilterNext { .. } => "FilterNext".to_string(),
1769 MutationSpec::MapUnwrapOr { .. } => "MapUnwrapOr".to_string(),
1770 MutationSpec::ReplaceExpr { .. } => "ReplaceExpr".to_string(),
1771 MutationSpec::RemoveStatement { .. } => "RemoveStatement".to_string(),
1772 MutationSpec::InsertStatement { .. } => "InsertStatement".to_string(),
1773 MutationSpec::ReplaceStatement { .. } => "ReplaceStatement".to_string(),
1774 MutationSpec::PluginTransform { .. } => "PluginTransform".to_string(),
1775 MutationSpec::DuplicateFunction { .. } => "DuplicateFunction".to_string(),
1776 MutationSpec::DuplicateStruct { .. } => "DuplicateStruct".to_string(),
1777 MutationSpec::DuplicateEnum { .. } => "DuplicateEnum".to_string(),
1778 MutationSpec::DuplicateModTree { .. } => "DuplicateModTree".to_string(),
1779 }
1780}
1781
1782#[cfg(test)]
1783mod tests {
1784 use super::*;
1785 use crate::executor::spec::{Scope, SelfParam, SymbolPath, Visibility};
1786 use ryo_analysis::testing::{ContextBuilder, ContextTestExt};
1787 use ryo_symbol::SymbolId;
1788
1789 fn dummy_id(index: u32) -> SymbolId {
1791 SymbolId::parse(&format!("{}v1", index)).expect("valid dummy id")
1792 }
1793
1794 fn execute_and_sync(
1797 executor: &BlueprintExecutor,
1798 blueprint: &ParallelBlueprint,
1799 ctx: &mut AnalysisContext,
1800 ) -> BlueprintResult {
1801 let result = executor.execute_v2(blueprint, ctx);
1802 if result.success {
1803 BlueprintExecutor::sync_files_and_rebuild(&result, ctx).unwrap();
1804 }
1805 result
1806 }
1807
1808 fn create_test_context() -> AnalysisContext {
1809 let code = r#"
1810struct Config {
1811 name: String,
1812}
1813
1814impl Config {
1815 fn new() -> Self {
1816 Self { name: String::new() }
1817 }
1818}
1819"#;
1820 ContextBuilder::new()
1821 .with_file("src/config.rs", code)
1822 .build()
1823 }
1824
1825 #[test]
1826 fn test_blueprint_executor_rename() {
1827 let mut ctx = create_test_context();
1828
1829 let symbol_id = ctx
1831 .registry()
1832 .lookup_by_name("Config")
1833 .expect("Config should exist in registry");
1834
1835 let specs = vec![MutationSpec::Rename {
1836 target: MutationTargetSymbol::ById(symbol_id),
1837 to: "AppConfig".to_string(),
1838 scope: Scope::Project,
1839 }];
1840 let blueprint = ParallelBlueprint::from_mutations(specs);
1841
1842 let executor = BlueprintExecutor::new();
1843 let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
1844
1845 assert!(exec_result.success);
1846 assert!(exec_result.total_changes > 0);
1847
1848 let file = ctx.test_file("src/config.rs").unwrap();
1850 let source = file.to_source().unwrap();
1851 assert!(source.contains("AppConfig"));
1852 assert!(!source.contains("struct Config"));
1853 }
1854
1855 #[test]
1856 fn test_blueprint_executor_add_derive() {
1857 let mut ctx = create_test_context();
1858
1859 let path = SymbolPath::parse("test_crate::config::Config").unwrap();
1861 let symbol_id = ctx.registry().lookup(&path).expect("Config should exist");
1862
1863 let specs = vec![MutationSpec::AddDerive {
1864 target: MutationTargetSymbol::ById(symbol_id),
1865 derives: vec!["Debug".to_string(), "Clone".to_string()],
1866 }];
1867 let blueprint = ParallelBlueprint::from_mutations(specs);
1868
1869 let executor = BlueprintExecutor::new();
1870 let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
1871
1872 assert!(exec_result.success);
1873
1874 let file = ctx.test_file("src/config.rs").unwrap();
1875 let source = file.to_source().unwrap();
1876 assert!(source.contains("derive"), "Expected derive in: {}", source);
1877 assert!(source.contains("Debug"));
1878 }
1879
1880 #[test]
1881 fn test_blueprint_executor_organize_imports() {
1882 let code = r#"
1883use std::collections::HashMap;
1884use std::io::Write;
1885use std::collections::HashSet;
1886use std::io::Read;
1887
1888fn main() {}
1889"#;
1890 let mut ctx = ContextBuilder::new().with_file("src/main.rs", code).build();
1891
1892 let specs = vec![MutationSpec::OrganizeImports {
1893 module_id: None,
1894 deduplicate: true,
1895 merge_groups: true,
1896 }];
1897 let blueprint = ParallelBlueprint::from_mutations(specs);
1898
1899 let executor = BlueprintExecutor::new();
1900 let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
1901
1902 assert!(exec_result.success);
1903 }
1904
1905 #[test]
1906 fn test_blueprint_with_conflicts_fails_when_not_ignored() {
1907 use ryo_symbol::{SymbolKind, SymbolPath, SymbolRegistry};
1908
1909 let mut ctx = create_test_context();
1910
1911 let mut symbol_registry = SymbolRegistry::new();
1913 let path_a = SymbolPath::parse("test_crate::A").unwrap();
1914 let symbol_a = symbol_registry
1915 .register(path_a, SymbolKind::Struct)
1916 .unwrap();
1917
1918 let specs = vec![
1920 MutationSpec::Rename {
1921 target: MutationTargetSymbol::ById(symbol_a),
1922 to: "B".to_string(),
1923 scope: Scope::Project,
1924 },
1925 MutationSpec::Rename {
1926 target: MutationTargetSymbol::ById(symbol_a),
1927 to: "C".to_string(),
1928 scope: Scope::Project,
1929 },
1930 ];
1931
1932 let blueprint = ParallelBlueprint::from_mutations(specs);
1933
1934 assert!(
1936 !blueprint.conflicts.is_empty(),
1937 "Blueprint should have conflicts"
1938 );
1939
1940 let mut executor = BlueprintExecutor::new();
1942 executor.ignore_conflicts = false;
1943 let result = execute_and_sync(&executor, &blueprint, &mut ctx);
1944
1945 assert!(
1947 !result.success,
1948 "Execution should fail when conflicts are not ignored"
1949 );
1950 assert!(result.error.is_some(), "Should have error message");
1951 assert!(
1952 result.error.unwrap().contains("conflict"),
1953 "Error should mention conflicts"
1954 );
1955 }
1956
1957 #[test]
1964 fn test_blueprint_executor_add_item_struct() {
1965 let mut ctx = ContextBuilder::new()
1966 .with_file("src/lib.rs", "// empty file\n")
1967 .build();
1968
1969 let spec = MutationSpec::AddItem {
1970 target: MutationTargetSymbol::ByPath(Box::new(
1971 SymbolPath::parse("test_crate").unwrap(),
1972 )),
1973 content: "pub struct Config {}".to_string(),
1974 position: super::super::spec::InsertPosition::Bottom,
1975 };
1976
1977 let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
1978 let executor = BlueprintExecutor::new();
1979 let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
1980
1981 assert!(
1982 exec_result.success,
1983 "AddItem struct failed: {:?}",
1984 exec_result.error
1985 );
1986
1987 let file = ctx.test_file("src/lib.rs").unwrap();
1988 let source = file.to_source().unwrap();
1989 assert!(
1990 source.contains("pub struct Config"),
1991 "Struct not added: {}",
1992 source
1993 );
1994 }
1995
1996 #[test]
1997 fn test_blueprint_executor_add_item_fn() {
1998 let mut ctx = ContextBuilder::new()
1999 .with_file("src/lib.rs", "// empty file\n")
2000 .build();
2001
2002 let spec = MutationSpec::AddItem {
2003 target: MutationTargetSymbol::ByPath(Box::new(
2004 SymbolPath::parse("test_crate").unwrap(),
2005 )),
2006 content: "fn helper() {}".to_string(),
2007 position: super::super::spec::InsertPosition::Bottom,
2008 };
2009
2010 let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
2011 let executor = BlueprintExecutor::new();
2012 let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
2013
2014 assert!(
2015 exec_result.success,
2016 "AddItem fn failed: {:?}",
2017 exec_result.error
2018 );
2019
2020 let file = ctx.test_file("src/lib.rs").unwrap();
2021 let source = file.to_source().unwrap();
2022 assert!(
2023 source.contains("fn helper"),
2024 "Function not added: {}",
2025 source
2026 );
2027 }
2028
2029 #[test]
2030 fn test_blueprint_executor_add_item_use() {
2031 let mut ctx = ContextBuilder::new()
2032 .with_file("src/lib.rs", "pub struct Dummy;\n")
2033 .build();
2034
2035 let spec = MutationSpec::AddItem {
2036 target: MutationTargetSymbol::ByPath(Box::new(
2037 SymbolPath::parse("test_crate").unwrap(),
2038 )),
2039 content: "use HashMap;".to_string(),
2040 position: super::super::spec::InsertPosition::Top,
2041 };
2042
2043 let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
2044 let executor = BlueprintExecutor::new();
2045 let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
2046
2047 assert!(
2048 exec_result.success,
2049 "AddItem use failed: {:?}",
2050 exec_result.error
2051 );
2052
2053 let file = ctx.test_file("src/lib.rs").unwrap();
2054 let source = file.to_source().unwrap();
2055 assert!(source.contains("use HashMap"), "Use not added: {}", source);
2056 }
2057
2058 #[test]
2059 fn test_blueprint_executor_add_item_impl() {
2060 let mut ctx = ContextBuilder::new()
2061 .with_file("src/lib.rs", "struct Foo {}\n")
2062 .build();
2063
2064 let spec = MutationSpec::AddItem {
2065 target: MutationTargetSymbol::ByPath(Box::new(
2066 SymbolPath::parse("test_crate").unwrap(),
2067 )),
2068 content: "impl Foo {}".to_string(),
2069 position: super::super::spec::InsertPosition::Bottom,
2070 };
2071
2072 let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
2073 let executor = BlueprintExecutor::new();
2074 let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
2075
2076 assert!(
2077 exec_result.success,
2078 "AddItem impl failed: {:?}",
2079 exec_result.error
2080 );
2081
2082 let file = ctx.test_file("src/lib.rs").unwrap();
2083 let source = file.to_source().unwrap();
2084 assert!(source.contains("impl Foo"), "Impl not added: {}", source);
2085 }
2086
2087 #[test]
2088 fn test_blueprint_executor_add_item_enum() {
2089 let mut ctx = ContextBuilder::new()
2090 .with_file("src/lib.rs", "// empty file\n")
2091 .build();
2092
2093 let spec = MutationSpec::AddItem {
2094 target: MutationTargetSymbol::ByPath(Box::new(
2095 SymbolPath::parse("test_crate").unwrap(),
2096 )),
2097 content: "pub enum Status { Pending, Active, Completed }".to_string(),
2098 position: super::super::spec::InsertPosition::Bottom,
2099 };
2100
2101 let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
2102 let executor = BlueprintExecutor::new();
2103 let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
2104
2105 assert!(
2106 exec_result.success,
2107 "AddItem enum failed: {:?}",
2108 exec_result.error
2109 );
2110
2111 let file = ctx.test_file("src/lib.rs").unwrap();
2112 let source = file.to_source().unwrap();
2113 assert!(
2114 source.contains("pub enum Status"),
2115 "Enum not added: {}",
2116 source
2117 );
2118 }
2119
2120 #[test]
2123 fn test_blueprint_executor_add_method_basic() {
2124 let mut ctx = ContextBuilder::new()
2125 .with_file("src/lib.rs", "pub struct Config {}\n\nimpl Config {}\n")
2126 .build();
2127
2128 let spec = MutationSpec::AddMethod {
2129 target: MutationTargetSymbol::ByKindAndName(
2130 crate::executor::ItemKind::Impl,
2131 "Config".to_string(),
2132 ),
2133 method_name: "new".to_string(),
2134 params: vec![],
2135 return_type: Some("Self".to_string()),
2136 body: "Self {}".to_string(),
2137 is_pub: true,
2138 self_param: None,
2139 };
2140
2141 let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
2142 let executor = BlueprintExecutor::new();
2143 let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
2144
2145 assert!(
2146 exec_result.success,
2147 "AddMethod basic failed: {:?}",
2148 exec_result.error
2149 );
2150
2151 let file = ctx.test_file("src/lib.rs").unwrap();
2152 let source = file.to_source().unwrap();
2153 assert!(
2154 source.contains("pub fn new"),
2155 "Method not added: {}",
2156 source
2157 );
2158 assert!(
2159 source.contains("-> Self"),
2160 "Return type not added: {}",
2161 source
2162 );
2163 }
2164
2165 #[test]
2166 fn test_blueprint_executor_add_method_with_self() {
2167 let mut ctx = ContextBuilder::new()
2168 .with_file(
2169 "src/lib.rs",
2170 "pub struct Counter { value: u32 }\n\nimpl Counter {}\n",
2171 )
2172 .build();
2173
2174 let spec = MutationSpec::AddMethod {
2175 target: MutationTargetSymbol::ByKindAndName(
2176 crate::executor::ItemKind::Impl,
2177 "Counter".to_string(),
2178 ),
2179 method_name: "get".to_string(),
2180 params: vec![],
2181 return_type: Some("u32".to_string()),
2182 body: "self.value".to_string(),
2183 is_pub: true,
2184 self_param: Some(SelfParam::Ref),
2185 };
2186
2187 let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
2188 let executor = BlueprintExecutor::new();
2189 let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
2190
2191 assert!(
2192 exec_result.success,
2193 "AddMethod with_self failed: {:?}",
2194 exec_result.error
2195 );
2196
2197 let file = ctx.test_file("src/lib.rs").unwrap();
2198 let source = file.to_source().unwrap();
2199 assert!(source.contains("&self"), "Self param not added: {}", source);
2200 assert!(source.contains("fn get"), "Method not added: {}", source);
2201 }
2202
2203 #[test]
2204 fn test_blueprint_executor_add_method_with_mut_self() {
2205 let mut ctx = ContextBuilder::new()
2206 .with_file(
2207 "src/lib.rs",
2208 "pub struct Counter { value: u32 }\n\nimpl Counter {}\n",
2209 )
2210 .build();
2211
2212 let spec = MutationSpec::AddMethod {
2213 target: MutationTargetSymbol::ByKindAndName(
2214 crate::executor::ItemKind::Impl,
2215 "Counter".to_string(),
2216 ),
2217 method_name: "increment".to_string(),
2218 params: vec![],
2219 return_type: None,
2220 body: "self.value += 1".to_string(),
2221 is_pub: true,
2222 self_param: Some(SelfParam::Mut),
2223 };
2224
2225 let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
2226 let executor = BlueprintExecutor::new();
2227 let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
2228
2229 assert!(
2230 exec_result.success,
2231 "AddMethod with_mut_self failed: {:?}",
2232 exec_result.error
2233 );
2234
2235 let file = ctx.test_file("src/lib.rs").unwrap();
2236 let source = file.to_source().unwrap();
2237 assert!(
2238 source.contains("&mut self"),
2239 "Mut self param not added: {}",
2240 source
2241 );
2242 assert!(
2243 source.contains("fn increment"),
2244 "Method not added: {}",
2245 source
2246 );
2247 }
2248
2249 #[test]
2250 fn test_blueprint_executor_add_method_with_params() {
2251 let mut ctx = ContextBuilder::new()
2252 .with_file(
2253 "src/lib.rs",
2254 "pub struct Calculator {}\n\nimpl Calculator {}\n",
2255 )
2256 .build();
2257
2258 let spec = MutationSpec::AddMethod {
2259 target: MutationTargetSymbol::ByKindAndName(
2260 crate::executor::ItemKind::Impl,
2261 "Calculator".to_string(),
2262 ),
2263 method_name: "add".to_string(),
2264 params: vec![
2265 ("a".to_string(), "i32".to_string()),
2266 ("b".to_string(), "i32".to_string()),
2267 ],
2268 return_type: Some("i32".to_string()),
2269 body: "a + b".to_string(),
2270 is_pub: true,
2271 self_param: None,
2272 };
2273
2274 let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
2275 let executor = BlueprintExecutor::new();
2276 let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
2277
2278 assert!(
2279 exec_result.success,
2280 "AddMethod with_params failed: {:?}",
2281 exec_result.error
2282 );
2283
2284 let file = ctx.test_file("src/lib.rs").unwrap();
2285 let source = file.to_source().unwrap();
2286 assert!(source.contains("fn add"), "Method not added: {}", source);
2287 assert!(source.contains("a: i32"), "Param a not added: {}", source);
2288 assert!(source.contains("b: i32"), "Param b not added: {}", source);
2289 }
2290
2291 #[test]
2294 fn test_blueprint_executor_multi_file_rename() {
2295 let mut ctx = ContextBuilder::new()
2296 .with_file(
2297 "src/lib.rs",
2298 r#"mod models;
2299
2300fn process(task: Task) -> Task {
2301 task
2302}
2303"#,
2304 )
2305 .with_file(
2306 "src/models.rs",
2307 r#"pub struct Task {
2308 pub id: u32,
2309 pub name: String,
2310}
2311"#,
2312 )
2313 .build();
2314
2315 let symbol_id = ctx
2317 .registry()
2318 .lookup_by_name("Task")
2319 .expect("Task should exist in registry");
2320
2321 let spec = MutationSpec::Rename {
2323 target: MutationTargetSymbol::ById(symbol_id),
2324 to: "TodoItem".to_string(),
2325 scope: Scope::Project,
2326 };
2327
2328 let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
2329 let executor = BlueprintExecutor::new();
2330 let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
2331
2332 assert!(
2333 exec_result.success,
2334 "Multi-file rename failed: {:?}",
2335 exec_result.error
2336 );
2337
2338 let lib = ctx.test_file("src/lib.rs").unwrap();
2340 let lib_source = lib.to_source().unwrap();
2341 assert!(
2342 lib_source.contains("task: TodoItem"),
2343 "lib.rs type not renamed: {}",
2344 lib_source
2345 );
2346 assert!(
2347 lib_source.contains("-> TodoItem"),
2348 "lib.rs return type not renamed: {}",
2349 lib_source
2350 );
2351
2352 let models = ctx.test_file("src/models.rs").unwrap();
2354 let models_source = models.to_source().unwrap();
2355 assert!(
2356 models_source.contains("struct TodoItem"),
2357 "models.rs struct not renamed: {}",
2358 models_source
2359 );
2360 assert!(
2361 !models_source.contains("struct Task"),
2362 "models.rs still has struct Task: {}",
2363 models_source
2364 );
2365 }
2366
2367 #[test]
2368 fn test_blueprint_executor_multi_file_add_items() {
2369 let mut ctx = ContextBuilder::new()
2370 .with_file("src/lib.rs", "pub mod models;\n")
2371 .with_file("src/models.rs", "pub struct Placeholder;\n")
2372 .build();
2373
2374 let spec1 = MutationSpec::AddItem {
2376 target: MutationTargetSymbol::ByPath(Box::new(
2377 SymbolPath::parse("test_crate::models").unwrap(),
2378 )),
2379 content: "pub struct User { id: u32 }".to_string(),
2380 position: super::super::spec::InsertPosition::Bottom,
2381 };
2382
2383 let spec2 = MutationSpec::AddItem {
2385 target: MutationTargetSymbol::ByPath(Box::new(
2386 SymbolPath::parse("test_crate").unwrap(),
2387 )),
2388 content: "use models::User;".to_string(),
2389 position: super::super::spec::InsertPosition::Bottom,
2390 };
2391
2392 let blueprint = ParallelBlueprint::from_mutations(vec![spec1, spec2]);
2393 let executor = BlueprintExecutor::new();
2394 let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
2395
2396 assert!(
2397 exec_result.success,
2398 "Multi-file add items failed: {:?}",
2399 exec_result.error
2400 );
2401
2402 let models = ctx.test_file("src/models.rs").unwrap();
2404 let models_source = models.to_source().unwrap();
2405 assert!(
2406 models_source.contains("pub struct User"),
2407 "User not added to models.rs: {}",
2408 models_source
2409 );
2410
2411 let lib = ctx.test_file("src/lib.rs").unwrap();
2413 let lib_source = lib.to_source().unwrap();
2414 assert!(
2415 lib_source.contains("use models::User"),
2416 "Use not added to lib.rs: {}",
2417 lib_source
2418 );
2419 }
2420
2421 #[test]
2424 fn test_blueprint_executor_add_item_generic_struct() {
2425 let mut ctx = ContextBuilder::new()
2426 .with_file("src/lib.rs", "// empty file\n")
2427 .build();
2428
2429 let spec = MutationSpec::AddItem {
2430 target: MutationTargetSymbol::ByPath(Box::new(
2431 SymbolPath::parse("test_crate").unwrap(),
2432 )),
2433 content: "pub struct Container<T> { value: T }".to_string(),
2434 position: super::super::spec::InsertPosition::Bottom,
2435 };
2436
2437 let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
2438 let executor = BlueprintExecutor::new();
2439 let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
2440
2441 assert!(
2442 exec_result.success,
2443 "AddItem generic struct failed: {:?}",
2444 exec_result.error
2445 );
2446
2447 let file = ctx.test_file("src/lib.rs").unwrap();
2448 let source = file.to_source().unwrap();
2449 assert!(
2450 source.contains("struct Container"),
2451 "Generic struct not added: {}",
2452 source
2453 );
2454 }
2455
2456 #[test]
2457 fn test_blueprint_executor_add_item_async_fn() {
2458 let mut ctx = ContextBuilder::new()
2459 .with_file("src/lib.rs", "// empty file\n")
2460 .build();
2461
2462 let spec = MutationSpec::AddItem {
2463 target: MutationTargetSymbol::ByPath(Box::new(
2464 SymbolPath::parse("test_crate").unwrap(),
2465 )),
2466 content: "pub async fn fetch_data() -> String { String::new() }".to_string(),
2467 position: super::super::spec::InsertPosition::Bottom,
2468 };
2469
2470 let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
2471 let executor = BlueprintExecutor::new();
2472 let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
2473
2474 assert!(
2475 exec_result.success,
2476 "AddItem async fn failed: {:?}",
2477 exec_result.error
2478 );
2479
2480 let file = ctx.test_file("src/lib.rs").unwrap();
2481 let source = file.to_source().unwrap();
2482 assert!(
2483 source.contains("fn fetch_data"),
2484 "Async fn not added: {}",
2485 source
2486 );
2487 }
2488
2489 #[test]
2490 fn test_blueprint_executor_add_field_to_generic_struct() {
2491 use ryo_analysis::SymbolKind;
2492
2493 let mut ctx = ContextBuilder::new()
2494 .with_file("src/lib.rs", "pub struct Wrapper<T> { inner: T }\n")
2495 .build();
2496
2497 let symbol_id = ctx
2499 .registry
2500 .iter()
2501 .find(|(id, path)| {
2502 path.name() == "Wrapper" && ctx.registry.kind(*id) == Some(SymbolKind::Struct)
2503 })
2504 .map(|(id, _)| id)
2505 .expect("Wrapper struct not found in registry");
2506
2507 let spec = MutationSpec::AddField {
2508 target: MutationTargetSymbol::ById(symbol_id),
2509 field_name: "count".to_string(),
2510 field_type: "usize".to_string(),
2511 visibility: Visibility::Pub,
2512 };
2513
2514 let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
2515 let executor = BlueprintExecutor::new();
2516 let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
2517
2518 assert!(
2519 exec_result.success,
2520 "AddField to generic struct failed: {:?}",
2521 exec_result.error
2522 );
2523
2524 let file = ctx.test_file("src/lib.rs").unwrap();
2525 let source = file.to_source().unwrap();
2526 assert!(
2527 source.contains("pub count: usize"),
2528 "Field not added to generic struct: {}",
2529 source
2530 );
2531 }
2532
2533 #[test]
2534 fn test_blueprint_executor_add_derive_to_generic_struct() {
2535 let mut ctx = ContextBuilder::new()
2536 .with_file(
2537 "src/lib.rs",
2538 "pub struct Pair<T, U> { first: T, second: U }\n",
2539 )
2540 .build();
2541
2542 let path = SymbolPath::parse("test_crate::Pair").unwrap();
2544 let symbol_id = ctx.registry().lookup(&path).expect("Pair should exist");
2545
2546 let spec = MutationSpec::AddDerive {
2547 target: MutationTargetSymbol::ById(symbol_id),
2548 derives: vec!["Debug".to_string(), "Clone".to_string()],
2549 };
2550
2551 let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
2552 let executor = BlueprintExecutor::new();
2553 let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
2554
2555 assert!(
2556 exec_result.success,
2557 "AddDerive to generic struct failed: {:?}",
2558 exec_result.error
2559 );
2560
2561 let file = ctx.test_file("src/lib.rs").unwrap();
2562 let source = file.to_source().unwrap();
2563 assert!(
2564 source.contains("Debug"),
2565 "Debug derive not added: {}",
2566 source
2567 );
2568 assert!(
2569 source.contains("Clone"),
2570 "Clone derive not added: {}",
2571 source
2572 );
2573 }
2574
2575 #[test]
2580 fn test_blueprint_executor_create_mod_declaration() {
2581 let mut ctx = ContextBuilder::new()
2582 .with_file("src/lib.rs", "use std::io;\n\nfn main() {}\n")
2583 .build();
2584
2585 let spec = MutationSpec::CreateMod {
2586 target: MutationTargetSymbol::ByPath(Box::new(
2587 SymbolPath::parse("test_crate").unwrap(),
2588 )),
2589 mod_name: "models".to_string(),
2590 content: String::new(),
2591 is_pub: false,
2592 };
2593
2594 let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
2595 let executor = BlueprintExecutor::new();
2596 let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
2597
2598 assert!(
2599 exec_result.success,
2600 "CreateMod failed: {:?}",
2601 exec_result.error
2602 );
2603
2604 let file = ctx.test_file("src/lib.rs").unwrap();
2605 let source = file.to_source().unwrap();
2606 assert!(source.contains("mod models;"), "Mod not added: {}", source);
2607 }
2608
2609 #[test]
2610 fn test_blueprint_executor_create_pub_mod_declaration() {
2611 let mut ctx = ContextBuilder::new()
2612 .with_file("src/lib.rs", "fn main() {}\n")
2613 .build();
2614
2615 let spec = MutationSpec::CreateMod {
2616 target: MutationTargetSymbol::ByPath(Box::new(
2617 SymbolPath::parse("test_crate").unwrap(),
2618 )),
2619 mod_name: "api".to_string(),
2620 content: String::new(),
2621 is_pub: true,
2622 };
2623
2624 let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
2625 let executor = BlueprintExecutor::new();
2626 let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
2627
2628 assert!(
2629 exec_result.success,
2630 "CreateMod pub failed: {:?}",
2631 exec_result.error
2632 );
2633
2634 let file = ctx.test_file("src/lib.rs").unwrap();
2635 let source = file.to_source().unwrap();
2636 assert!(
2637 source.contains("pub mod api;"),
2638 "Pub mod not added: {}",
2639 source
2640 );
2641 }
2642
2643 #[test]
2644 fn test_blueprint_executor_create_file() {
2645 let mut ctx = ContextBuilder::new()
2647 .with_file("src/lib.rs", "// lib.rs\n")
2648 .build();
2649
2650 let spec = MutationSpec::CreateMod {
2651 target: MutationTargetSymbol::ByPath(Box::new(
2652 SymbolPath::parse("test_crate").unwrap(),
2653 )),
2654 mod_name: "models".to_string(),
2655 content: "pub struct Model { id: u32 }".to_string(),
2656 is_pub: true,
2657 };
2658
2659 let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
2660 let executor = BlueprintExecutor::new();
2661 let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
2662
2663 assert!(
2664 exec_result.success,
2665 "CreateFile failed: {:?}",
2666 exec_result.error
2667 );
2668
2669 assert!(ctx.test_file("src/models.rs").is_some(), "File not created");
2671
2672 let file = ctx.test_file("src/models.rs").unwrap();
2673 let source = file.to_source().unwrap();
2674 assert!(
2675 source.contains("struct Model"),
2676 "Content not correct: {}",
2677 source
2678 );
2679 }
2680
2681 #[test]
2682 fn test_blueprint_executor_create_module_workflow() {
2683 let mut ctx = ContextBuilder::new()
2684 .with_file("src/lib.rs", "fn main() {}\n")
2685 .build();
2686
2687 let spec = MutationSpec::CreateMod {
2689 target: MutationTargetSymbol::ByPath(Box::new(
2690 SymbolPath::parse("test_crate").unwrap(),
2691 )),
2692 mod_name: "utils".to_string(),
2693 content: "pub fn helper() {}".to_string(),
2694 is_pub: true,
2695 };
2696
2697 let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
2698 let executor = BlueprintExecutor::new();
2699 let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
2700
2701 assert!(
2702 exec_result.success,
2703 "Module workflow failed: {:?}",
2704 exec_result.error
2705 );
2706
2707 let lib = ctx.test_file("src/lib.rs").unwrap();
2709 let lib_source = lib.to_source().unwrap();
2710 assert!(
2711 lib_source.contains("pub mod utils;"),
2712 "Mod not added to lib: {}",
2713 lib_source
2714 );
2715
2716 let utils = ctx.test_file("src/utils.rs").unwrap();
2718 let utils_source = utils.to_source().unwrap();
2719 assert!(
2720 utils_source.contains("fn helper"),
2721 "Function not in utils: {}",
2722 utils_source
2723 );
2724 }
2725
2726 #[test]
2729 fn test_wavefront_execution_basic() {
2730 let mut ctx = create_test_context();
2731
2732 let path = SymbolPath::parse("test_crate::config::Config").unwrap();
2734 let symbol_id = ctx.registry().lookup(&path).expect("Config should exist");
2735
2736 let specs = vec![MutationSpec::AddDerive {
2737 target: MutationTargetSymbol::ById(symbol_id),
2738 derives: vec!["Debug".to_string()],
2739 }];
2740 let blueprint = ParallelBlueprint::from_mutations(specs);
2741
2742 let executor = BlueprintExecutor::new().with_strategy(ExecutionStrategy::Wavefront);
2743 let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
2744
2745 assert!(
2746 exec_result.success,
2747 "Wavefront basic failed: {:?}",
2748 exec_result.error
2749 );
2750 assert!(exec_result.total_changes > 0);
2751
2752 let file = ctx.test_file("src/config.rs").unwrap();
2753 let source = file.to_source().unwrap();
2754 assert!(source.contains("Debug"), "Derive not added: {}", source);
2755 }
2756
2757 #[test]
2758 fn test_wavefront_execution_multi_file() {
2759 let mut ctx = ContextBuilder::new()
2760 .with_file("src/lib.rs", "// lib\n")
2761 .with_file("src/models.rs", "// models\n")
2762 .build();
2763
2764 let spec1 = MutationSpec::AddItem {
2766 target: MutationTargetSymbol::ByPath(Box::new(
2767 SymbolPath::parse("test_crate::models").unwrap(),
2768 )),
2769 content: "pub struct User { id: u32 }".to_string(),
2770 position: super::super::spec::InsertPosition::Bottom,
2771 };
2772
2773 let spec2 = MutationSpec::AddItem {
2774 target: MutationTargetSymbol::ByPath(Box::new(
2775 SymbolPath::parse("test_crate").unwrap(),
2776 )),
2777 content: "fn main() {}".to_string(),
2778 position: super::super::spec::InsertPosition::Bottom,
2779 };
2780
2781 let blueprint = ParallelBlueprint::from_mutations(vec![spec1, spec2]);
2782 let executor = BlueprintExecutor::new().with_strategy(ExecutionStrategy::Wavefront);
2783 let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
2784
2785 assert!(
2786 exec_result.success,
2787 "Wavefront multi-file failed: {:?}",
2788 exec_result.error
2789 );
2790
2791 let models = ctx.test_file("src/models.rs").unwrap();
2792 assert!(
2793 models.to_source().unwrap().contains("pub struct User"),
2794 "User not added to models.rs"
2795 );
2796
2797 let lib = ctx.test_file("src/lib.rs").unwrap();
2798 assert!(
2799 lib.to_source().unwrap().contains("fn main"),
2800 "main not added to lib.rs"
2801 );
2802 }
2803
2804 #[test]
2805 fn test_wavefront_execution_with_dependencies() {
2806 let mut ctx = ContextBuilder::new()
2807 .with_file("src/lib.rs", "fn main() {}\n")
2808 .build();
2809
2810 let spec = MutationSpec::CreateMod {
2812 target: MutationTargetSymbol::ByPath(Box::new(
2813 SymbolPath::parse("test_crate").unwrap(),
2814 )),
2815 mod_name: "api".to_string(),
2816 content: "pub fn endpoint() {}".to_string(),
2817 is_pub: true,
2818 };
2819
2820 let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
2821 let executor = BlueprintExecutor::new().with_strategy(ExecutionStrategy::Wavefront);
2822 let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
2823
2824 assert!(
2825 exec_result.success,
2826 "Wavefront with deps failed: {:?}",
2827 exec_result.error
2828 );
2829
2830 let lib = ctx.test_file("src/lib.rs").unwrap();
2832 assert!(
2833 lib.to_source().unwrap().contains("pub mod api;"),
2834 "Mod not added to lib"
2835 );
2836
2837 let api = ctx.test_file("src/api.rs").unwrap();
2839 assert!(
2840 api.to_source().unwrap().contains("fn endpoint"),
2841 "Function not in api"
2842 );
2843 }
2844
2845 #[test]
2846 fn test_suggest_strategy() {
2847 let specs = vec![MutationSpec::AddDerive {
2849 target: MutationTargetSymbol::ById(dummy_id(1)),
2850 derives: vec!["Debug".to_string()],
2851 }];
2852 let blueprint = ParallelBlueprint::from_mutations(specs);
2853 assert_eq!(suggest_strategy(&blueprint), ExecutionStrategy::Sequential);
2854
2855 let specs = vec![
2857 MutationSpec::AddDerive {
2858 target: MutationTargetSymbol::ById(dummy_id(1)),
2859 derives: vec!["Debug".to_string()],
2860 },
2861 MutationSpec::AddDerive {
2862 target: MutationTargetSymbol::ById(dummy_id(1)),
2863 derives: vec!["Clone".to_string()],
2864 },
2865 ];
2866 let blueprint = ParallelBlueprint::from_mutations(specs);
2867 assert_eq!(suggest_strategy(&blueprint), ExecutionStrategy::Sequential);
2868
2869 let specs = vec![
2871 MutationSpec::AddDerive {
2872 target: MutationTargetSymbol::ById(dummy_id(1)),
2873 derives: vec!["Debug".to_string()],
2874 },
2875 MutationSpec::AddDerive {
2876 target: MutationTargetSymbol::ById(dummy_id(1)),
2877 derives: vec!["Clone".to_string()],
2878 },
2879 MutationSpec::AddDerive {
2880 target: MutationTargetSymbol::ById(dummy_id(1)),
2881 derives: vec!["Default".to_string()],
2882 },
2883 MutationSpec::AddDerive {
2884 target: MutationTargetSymbol::ById(dummy_id(1)),
2885 derives: vec!["Hash".to_string()],
2886 },
2887 ];
2888 let blueprint = ParallelBlueprint::from_mutations(specs);
2889 assert_eq!(suggest_strategy(&blueprint), ExecutionStrategy::Wavefront);
2890 }
2891
2892 #[test]
2893 #[ignore = "V1 path disabled - needs V2 migration"]
2894 fn test_sequential_and_wavefront_produce_same_result() {
2895 let code = r#"
2896struct A {}
2897struct B {}
2898struct C {}
2899"#;
2900
2901 let specs = vec![
2902 MutationSpec::AddDerive {
2903 target: MutationTargetSymbol::ById(dummy_id(1)),
2904 derives: vec!["Debug".to_string()],
2905 },
2906 MutationSpec::AddDerive {
2907 target: MutationTargetSymbol::ById(dummy_id(1)),
2908 derives: vec!["Clone".to_string()],
2909 },
2910 MutationSpec::AddDerive {
2911 target: MutationTargetSymbol::ById(dummy_id(1)),
2912 derives: vec!["Default".to_string()],
2913 },
2914 ];
2915 let blueprint = ParallelBlueprint::from_mutations(specs.clone());
2916
2917 let mut ctx_seq = ContextBuilder::new().with_file("src/lib.rs", code).build();
2919 let executor_seq = BlueprintExecutor::new().with_strategy(ExecutionStrategy::Sequential);
2920 let result_seq = execute_and_sync(&executor_seq, &blueprint, &mut ctx_seq);
2921
2922 let mut ctx_wave = ContextBuilder::new().with_file("src/lib.rs", code).build();
2924 let executor_wave = BlueprintExecutor::new().with_strategy(ExecutionStrategy::Wavefront);
2925 let result_wave = execute_and_sync(&executor_wave, &blueprint, &mut ctx_wave);
2926
2927 assert!(result_seq.success, "Sequential failed");
2929 assert!(result_wave.success, "Wavefront failed");
2930
2931 let source_seq = ctx_seq
2933 .test_file("src/lib.rs")
2934 .unwrap()
2935 .to_source()
2936 .unwrap();
2937 let source_wave = ctx_wave
2938 .test_file("src/lib.rs")
2939 .unwrap()
2940 .to_source()
2941 .unwrap();
2942
2943 assert!(source_seq.contains("Debug"), "Sequential missing Debug");
2944 assert!(source_seq.contains("Clone"), "Sequential missing Clone");
2945 assert!(source_seq.contains("Default"), "Sequential missing Default");
2946 assert!(source_wave.contains("Debug"), "Wavefront missing Debug");
2947 assert!(source_wave.contains("Clone"), "Wavefront missing Clone");
2948 assert!(source_wave.contains("Default"), "Wavefront missing Default");
2949 }
2950
2951 #[test]
2952 #[ignore = "flaky: µs-level timing comparison depends on CPU load"]
2953 fn test_execute_v2_without_sync_is_faster() {
2954 use ryo_analysis::SymbolKind;
2955 use std::time::Instant;
2956
2957 let code = r#"
2959pub struct Config { name: String, value: i32 }
2960pub struct User { id: u64, name: String, email: String }
2961pub struct Order { id: u64, user_id: u64, total: f64 }
2962pub enum Status { Pending, Active, Completed, Failed }
2963pub trait Processor { fn process(&self); }
2964impl Processor for Config { fn process(&self) {} }
2965impl Processor for User { fn process(&self) {} }
2966"#;
2967
2968 fn create_specs(ctx: &ryo_analysis::AnalysisContext) -> Vec<MutationSpec> {
2970 let config_id = ctx
2971 .registry
2972 .iter()
2973 .find(|(id, path)| {
2974 path.name() == "Config" && ctx.registry.kind(*id) == Some(SymbolKind::Struct)
2975 })
2976 .map(|(id, _)| id)
2977 .expect("Config not found");
2978
2979 let user_id = ctx
2980 .registry
2981 .iter()
2982 .find(|(id, path)| {
2983 path.name() == "User" && ctx.registry.kind(*id) == Some(SymbolKind::Struct)
2984 })
2985 .map(|(id, _)| id)
2986 .expect("User not found");
2987
2988 vec![
2989 MutationSpec::AddField {
2990 target: MutationTargetSymbol::ById(config_id),
2991 field_name: "enabled".to_string(),
2992 field_type: "bool".to_string(),
2993 visibility: Visibility::Pub,
2994 },
2995 MutationSpec::AddDerive {
2996 target: MutationTargetSymbol::ById(user_id),
2997 derives: vec!["Debug".to_string(), "Clone".to_string()],
2998 },
2999 ]
3000 }
3001
3002 let iterations = 10;
3004 let mut execute_only_times = Vec::with_capacity(iterations);
3005
3006 for _ in 0..iterations {
3007 let mut ctx = ContextBuilder::new().with_file("src/lib.rs", code).build();
3008 let specs = create_specs(&ctx);
3009 let blueprint = ParallelBlueprint::from_mutations(specs);
3010 let executor = BlueprintExecutor::new();
3011
3012 let t0 = Instant::now();
3013 let result = executor.execute_v2(&blueprint, &mut ctx);
3014 execute_only_times.push(t0.elapsed());
3015
3016 assert!(result.success);
3017 }
3018
3019 let mut execute_with_sync_times = Vec::with_capacity(iterations);
3021
3022 for _ in 0..iterations {
3023 let mut ctx = ContextBuilder::new().with_file("src/lib.rs", code).build();
3024 let specs = create_specs(&ctx);
3025 let blueprint = ParallelBlueprint::from_mutations(specs);
3026 let executor = BlueprintExecutor::new();
3027
3028 let t0 = Instant::now();
3029 let result = executor.execute_v2(&blueprint, &mut ctx);
3030 BlueprintExecutor::sync_files_and_rebuild(&result, &mut ctx).unwrap();
3031 execute_with_sync_times.push(t0.elapsed());
3032
3033 assert!(result.success);
3034 }
3035
3036 let avg_execute_only: u128 = execute_only_times
3037 .iter()
3038 .map(|d| d.as_micros())
3039 .sum::<u128>()
3040 / iterations as u128;
3041 let avg_with_sync: u128 = execute_with_sync_times
3042 .iter()
3043 .map(|d| d.as_micros())
3044 .sum::<u128>()
3045 / iterations as u128;
3046
3047 eprintln!(
3048 "execute_v2 only: {}µs avg, execute_v2 + sync: {}µs avg, sync overhead: {:.1}x",
3049 avg_execute_only,
3050 avg_with_sync,
3051 avg_with_sync as f64 / avg_execute_only as f64
3052 );
3053
3054 assert!(
3056 avg_execute_only < avg_with_sync,
3057 "execute_v2 only ({}µs) should be faster than with sync ({}µs)",
3058 avg_execute_only,
3059 avg_with_sync
3060 );
3061 }
3062}