Skip to main content

ryo_executor/executor/registry/
mod.rs

1//! MutationRegistry: Central registry for MutationSpec → Mutation conversion
2//!
3//! The Registry pattern distributes the conversion logic that was previously
4//! concentrated in `BlueprintExecutor::convert_and_apply()` (2,400+ lines)
5//! into separate Converter implementations.
6//!
7//! # Architecture
8//!
9//! ```text
10//! MutationSpec
11//!    │
12//!    ▼ registry.convert(spec)
13//! MutationRegistry
14//!    ├─ converters: HashMap<kind, Box<dyn MutationConverter>>
15//!    │
16//!    ▼ find converter by spec.kind_name()
17//! MutationConverter (trait)
18//!    │
19//!    ▼ convert(spec) → Box<dyn Mutation>
20//! Mutation
21//!    │
22//!    ▼ apply(file) → changes
23//! ```
24
25mod converter;
26pub mod converters;
27
28pub use converter::{
29    opt_resolve_file_path_from_symbol, resolve_file_path_from_symbol, ApplyResult, ConvertError,
30    MutationConverter, ResolvedMutation,
31};
32
33use crate::engine::ASTRegApply;
34use crate::executor::spec::MutationSpec;
35use ryo_analysis::{AnalysisContext, GraphChecker};
36use std::collections::HashMap;
37
38/// Central registry for MutationSpec → Mutation conversion
39///
40/// Routes each MutationSpec to its appropriate Converter based on kind_name().
41pub struct MutationRegistry {
42    converters: HashMap<&'static str, Box<dyn MutationConverter>>,
43}
44
45impl std::fmt::Debug for MutationRegistry {
46    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47        f.debug_struct("MutationRegistry")
48            .field("registered_kinds", &self.registered_kinds())
49            .finish()
50    }
51}
52
53impl MutationRegistry {
54    /// Create a new registry with all built-in converters registered
55    ///
56    /// Uses `register_all()` for converters that handle multiple spec kinds,
57    /// which automatically registers all kinds from `spec_kinds()`.
58    /// This prevents the "forgot to register" bug.
59    pub fn new() -> Self {
60        let mut registry = Self {
61            converters: HashMap::new(),
62        };
63
64        // Phase 1: Basic converters (single or few kinds)
65        registry.register("Rename", Box::new(converters::RenameConverter::new()));
66        registry.register(
67            "ChangeVisibility",
68            Box::new(converters::VisibilityConverter::new()),
69        );
70        registry.register_all::<converters::FieldConverter>(); // AddField, RemoveField
71        registry.register_all::<converters::DeriveConverter>(); // AddDerive, RemoveDerive
72
73        // Phase 2: Complex converters
74        registry.register_all::<converters::EnumConverter>(); // AddVariant, RemoveVariant
75        registry.register_all::<converters::RemoveConverter>(); // RemoveItem, ReplaceCode
76        registry.register_all::<converters::MethodConverter>(); // AddMethod, RemoveMethod
77        registry.register_all::<converters::ModuleConverter>(); // AddMod, RemoveMod, CreateMod
78        registry.register("AddItem", Box::new(converters::AddItemConverter::new()));
79
80        // Phase 3: Idiom converters (15 variants - biggest win!)
81        registry.register_all::<converters::IdiomConverter>();
82
83        // Phase 3: Other converters
84        registry.register_all::<converters::TraitConverter>(); // ExtractTrait, InlineTrait
85        registry.register("MoveItem", Box::new(converters::MoveConverter::new()));
86        registry.register(
87            "PluginTransform",
88            Box::new(converters::PluginConverter::new()),
89        );
90        registry.register_all::<converters::StmtConverter>(); // ReplaceExpr, RemoveStatement, etc.
91        registry.register_all::<converters::MatchArmConverter>(); // AddMatchArm, RemoveMatchArm
92        registry.register_all::<converters::StructLiteralFieldConverter>(); // Add/RemoveStructLiteralField
93        registry.register_all::<converters::DuplicateConverter>(); // DuplicateFunction, etc.
94        registry.register_all::<converters::SpecAliasConverter>(); // AddSpec, RemoveSpec, ValidateSpec
95
96        registry
97    }
98
99    /// Register a converter for a single spec kind
100    ///
101    /// Note: Each spec kind can only have one converter.
102    /// For converters handling multiple spec kinds, use `register_all()`.
103    pub fn register(&mut self, kind: &'static str, converter: Box<dyn MutationConverter>) {
104        self.converters.insert(kind, converter);
105    }
106
107    /// Register a converter for all its spec_kinds() automatically.
108    ///
109    /// This method creates a new instance of the converter for each spec kind
110    /// it handles. Requires the converter to implement `Default`.
111    ///
112    /// # Example
113    ///
114    /// ```ignore
115    /// // Instead of:
116    /// registry.register("FilterNext", Box::new(IdiomConverter::new()));
117    /// registry.register("MapUnwrapOr", Box::new(IdiomConverter::new()));
118    /// // ... 15 more lines
119    ///
120    /// // Use:
121    /// registry.register_all::<IdiomConverter>();
122    /// ```
123    pub fn register_all<C: MutationConverter + Default + 'static>(&mut self) {
124        let temp = C::default();
125        for kind in temp.spec_kinds() {
126            self.converters.insert(*kind, Box::new(C::default()));
127        }
128    }
129
130    /// Check if this registry can handle the given spec
131    pub fn can_handle(&self, spec: &MutationSpec) -> bool {
132        self.converters.contains_key(spec.kind_name())
133    }
134
135    /// Get the converter for a spec, if registered
136    pub fn get(&self, spec: &MutationSpec) -> Option<&dyn MutationConverter> {
137        self.converters.get(spec.kind_name()).map(|c| c.as_ref())
138    }
139
140    /// Convert a MutationSpec to execution units (V2 API)
141    ///
142    /// Returns a vector of ASTRegApply mutations that implement the spec.
143    /// One spec may expand to multiple execution units.
144    ///
145    /// # Returns
146    ///
147    /// - `Ok(mutations)` - Vector of mutations to execute
148    /// - `Err(V2NotSupported)` - Converter doesn't implement convert_v2 yet
149    /// - `Err(UnknownSpec)` - No converter registered for this spec kind
150    pub fn convert_v2(
151        &self,
152        spec: &MutationSpec,
153        ctx: &AnalysisContext,
154    ) -> Result<Vec<Box<dyn ASTRegApply>>, ConvertError> {
155        let converter = self
156            .converters
157            .get(spec.kind_name())
158            .ok_or_else(|| ConvertError::UnknownSpec(spec.kind_name().to_string()))?;
159
160        converter.convert_v2(spec, ctx)
161    }
162
163    /// Pre-check a MutationSpec before applying.
164    ///
165    /// Uses GraphChecker to validate that targets exist before mutation.
166    /// This catches errors early (e.g., field not found, type not found)
167    /// without running `cargo check`.
168    ///
169    /// # Checks performed
170    ///
171    /// | Spec Kind | Check |
172    /// |-----------|-------|
173    /// | Rename | Target symbol exists |
174    /// | AddField/RemoveField | Struct exists |
175    /// | AddDerive/RemoveDerive | Target type exists |
176    /// | AddVariant/RemoveVariant | Enum exists |
177    /// | AddMethod/RemoveMethod | Target type exists |
178    /// | ChangeVisibility | Target exists |
179    pub fn pre_check(
180        &self,
181        spec: &MutationSpec,
182        ctx: &AnalysisContext,
183    ) -> Result<(), ConvertError> {
184        let _checker = GraphChecker::new(ctx.code_graph(), ctx.typeflow_graph(), ctx.registry());
185
186        match spec {
187            // === Symbol existence and uniqueness checks ===
188            // symbol_id is now required, so we trust it (O(1) access).
189            // No uniqueness check needed.
190            MutationSpec::Rename { .. } => {}
191
192            // AddField/RemoveField: symbol_id is required, no uniqueness check needed
193            MutationSpec::AddField { .. } | MutationSpec::RemoveField { .. } => {}
194
195            // AddDerive/RemoveDerive: symbol_id is required, no uniqueness check needed
196            MutationSpec::AddDerive { .. } | MutationSpec::RemoveDerive { .. } => {}
197
198            // AddVariant: symbol_id is required, no uniqueness check needed
199            MutationSpec::AddVariant { .. } => {}
200
201            // RemoveVariant: symbol_id is required, no uniqueness check needed
202            MutationSpec::RemoveVariant { .. } => {}
203
204            MutationSpec::AddMethod {
205                target: target_symbol,
206                ..
207            } => {
208                // AddMethod uses target_symbol: MutationTargetSymbol (lazy resolution)
209                // Pre-check validation happens at converter level
210                let _ = target_symbol; // Suppress unused warning
211            }
212
213            MutationSpec::RemoveMethod { .. } => {
214                // SymbolId is required, no name-based pre-check needed
215            }
216
217            MutationSpec::ChangeVisibility { .. } => {
218                // SymbolId is required, no name-based pre-check needed
219            }
220
221            // === Mutations that don't need pre-check ===
222            // SymbolId is required for RemoveItem, no name-based pre-check needed
223            MutationSpec::RemoveItem { .. } => {
224                // SymbolId is required, no name-based pre-check needed
225            }
226
227            // ReplaceCode targets a top-level item by SymbolId; the raw
228            // bytes are routed to a PureItem::Verbatim during apply, no
229            // name-based pre-check needed.
230            MutationSpec::ReplaceCode { .. } => {}
231
232            // These create new items or operate on files directly
233            MutationSpec::AddItem { .. }
234            | MutationSpec::RemoveMod { .. }
235            | MutationSpec::CreateMod { .. }
236            | MutationSpec::AddSpec { .. }
237            | MutationSpec::AddMatchArm { .. }
238            | MutationSpec::RemoveMatchArm { .. }
239            | MutationSpec::ReplaceMatchArm { .. }
240            | MutationSpec::AddStructLiteralField { .. }
241            | MutationSpec::RemoveStructLiteralField { .. } => {
242                // No pre-check needed
243            }
244
245            // === Idiom transformations ===
246            // These operate on code patterns, not specific symbols
247            MutationSpec::OrganizeImports { .. }
248            | MutationSpec::LoopToIterator { .. }
249            | MutationSpec::UnwrapToQuestion { .. }
250            | MutationSpec::AssignOp { .. }
251            | MutationSpec::BoolSimplify { .. }
252            | MutationSpec::CloneOnCopy { .. }
253            | MutationSpec::CollapsibleIf { .. }
254            | MutationSpec::ComparisonToMethod { .. }
255            | MutationSpec::RedundantClosure { .. }
256            | MutationSpec::IntroduceVariable { .. }
257            | MutationSpec::ManualMap { .. }
258            | MutationSpec::MatchToIfLet { .. }
259            | MutationSpec::FilterNext { .. }
260            | MutationSpec::MapUnwrapOr { .. } => {
261                // No pre-check for idiom transformations
262            }
263
264            // === Spec operations ===
265            MutationSpec::RemoveSpec { .. } => {
266                // SymbolId is required, no name-based pre-check needed
267            }
268
269            MutationSpec::ValidateSpec { .. } => {
270                // ValidateSpec is read-only, no pre-check needed
271            }
272
273            // === Other mutations ===
274            MutationSpec::ExtractTrait { .. }
275            | MutationSpec::InlineTrait { .. }
276            | MutationSpec::ReplaceType { .. }
277            | MutationSpec::EnumToTrait { .. }
278            | MutationSpec::MoveItem { .. }
279            | MutationSpec::PluginTransform { .. }
280            | MutationSpec::ReplaceExpr { .. }
281            | MutationSpec::RemoveStatement { .. }
282            | MutationSpec::InsertStatement { .. }
283            | MutationSpec::ReplaceStatement { .. }
284            | MutationSpec::DuplicateFunction { .. }
285            | MutationSpec::DuplicateStruct { .. }
286            | MutationSpec::DuplicateEnum { .. }
287            | MutationSpec::DuplicateModTree { .. }
288            | MutationSpec::NoOpArmToTodo { .. } => {
289                // No pre-check for these (or could be added later)
290            }
291        }
292
293        Ok(())
294    }
295
296    /// Get the number of registered converters
297    pub fn len(&self) -> usize {
298        self.converters.len()
299    }
300
301    /// Check if the registry is empty
302    pub fn is_empty(&self) -> bool {
303        self.converters.is_empty()
304    }
305
306    /// Get all registered spec kinds
307    pub fn registered_kinds(&self) -> Vec<&'static str> {
308        self.converters.keys().copied().collect()
309    }
310}
311
312impl Default for MutationRegistry {
313    fn default() -> Self {
314        Self::new()
315    }
316}
317
318#[cfg(test)]
319mod tests {
320    use super::*;
321
322    #[test]
323    fn test_registry_has_all_converters() {
324        let registry = MutationRegistry::new();
325        // All phases converters are registered
326        assert!(!registry.is_empty());
327        // Phase 1: Rename, AddField, RemoveField, ChangeVisibility, AddDerive, RemoveDerive (6)
328        // Phase 2: AddVariant, RemoveVariant, RemoveItem, AddMethod, RemoveMethod,
329        //          RemoveMod, CreateMod, AddItem (8) - Note: AddMod was consolidated into CreateMod
330        // Phase 3: 15 Idiom + 3 Trait (ExtractTrait, InlineTrait, EnumToTrait) + 1 Move + 1 Plugin
331        //          + 4 Stmt + 2 MatchArm + 2 StructLiteral + 4 Duplicate + 3 SpecAlias
332        //          (AddSpec, RemoveSpec, ValidateSpec) + 1 ReplaceCode (added to
333        //          RemoveConverter, so the count here goes up but Phase 2 still
334        //          lists RemoveItem separately below) + 1 Default = 37
335        // Note: MergeImplBlocks removed - RegistryGenerator auto-merges impl blocks
336        // Note: RemoveConverter now handles both RemoveItem and ReplaceCode
337        //       (+1 over previous count).
338        // Total spec kinds handled: 6 + 8 + 37 = 51
339        assert_eq!(registry.len(), 51);
340    }
341
342    #[test]
343    fn test_registry_can_handle_rename() {
344        use ryo_symbol::{SymbolKind, SymbolPath, SymbolRegistry};
345
346        let registry = MutationRegistry::new();
347        let mut sym_registry = SymbolRegistry::new();
348        let path = SymbolPath::parse("test_crate::old").unwrap();
349        let symbol_id = sym_registry.register(path, SymbolKind::Function).unwrap();
350
351        let spec = MutationSpec::Rename {
352            target: crate::executor::spec::MutationTargetSymbol::ById(symbol_id),
353            to: "new".into(),
354            scope: crate::executor::spec::Scope::Project,
355        };
356
357        assert!(registry.can_handle(&spec));
358    }
359
360    #[test]
361    fn test_registry_can_handle_field() {
362        use ryo_symbol::{SymbolKind, SymbolPath, SymbolRegistry};
363
364        let registry = MutationRegistry::new();
365        let mut sym_registry = SymbolRegistry::new();
366        let path = SymbolPath::parse("test_crate::Config").unwrap();
367        let symbol_id = sym_registry.register(path, SymbolKind::Struct).unwrap();
368
369        let add_spec = MutationSpec::AddField {
370            target: crate::executor::spec::MutationTargetSymbol::ById(symbol_id),
371            field_name: "timeout".into(),
372            field_type: "u64".into(),
373            visibility: crate::executor::spec::Visibility::Pub,
374        };
375        assert!(registry.can_handle(&add_spec));
376
377        let remove_spec = MutationSpec::RemoveField {
378            target: crate::executor::spec::MutationTargetSymbol::ById(symbol_id),
379            field_name: "timeout".into(),
380        };
381        assert!(registry.can_handle(&remove_spec));
382    }
383
384    #[test]
385    fn test_registry_can_handle_add_item() {
386        let registry = MutationRegistry::new();
387
388        // AddItem is registered (Phase 2)
389        let spec = MutationSpec::AddItem {
390            target: crate::executor::spec::MutationTargetSymbol::ByPath(Box::new(
391                crate::executor::spec::SymbolPath::parse("test_crate::lib").unwrap(),
392            )),
393            content: "struct Foo {}".into(),
394            position: crate::executor::spec::InsertPosition::Top,
395        };
396        assert!(registry.can_handle(&spec));
397    }
398
399    #[test]
400    fn test_registry_registered_kinds() {
401        let registry = MutationRegistry::new();
402        let kinds = registry.registered_kinds();
403
404        assert!(kinds.contains(&"Rename"));
405        assert!(kinds.contains(&"AddField"));
406        assert!(kinds.contains(&"RemoveField"));
407        assert!(kinds.contains(&"ChangeVisibility"));
408        assert!(kinds.contains(&"AddDerive"));
409        assert!(kinds.contains(&"RemoveDerive"));
410    }
411
412    /// Ensures all converter spec_kinds() are registered in MutationRegistry.
413    /// This test prevents the "forgot to register" bug where a converter
414    /// declares spec_kinds but they're not added to the registry.
415    #[test]
416    fn test_all_converter_spec_kinds_are_registered() {
417        use std::collections::HashSet;
418
419        let registry = MutationRegistry::new();
420        let registered: HashSet<&str> = registry.registered_kinds().into_iter().collect();
421
422        // Check IdiomConverter
423        let idiom = converters::IdiomConverter::new();
424        for kind in idiom.spec_kinds() {
425            assert!(
426                registered.contains(kind),
427                "IdiomConverter::spec_kinds() contains '{}' but NOT registered in MutationRegistry. \
428                Add: registry.register(\"{}\", Box::new(converters::IdiomConverter::new()));",
429                kind, kind
430            );
431        }
432
433        // Check RenameConverter
434        let rename = converters::RenameConverter::new();
435        for kind in rename.spec_kinds() {
436            assert!(
437                registered.contains(kind),
438                "RenameConverter::spec_kinds() contains '{}' but NOT registered",
439                kind
440            );
441        }
442
443        // Check FieldConverter
444        let field = converters::FieldConverter::new();
445        for kind in field.spec_kinds() {
446            assert!(
447                registered.contains(kind),
448                "FieldConverter::spec_kinds() contains '{}' but NOT registered",
449                kind
450            );
451        }
452
453        // Check EnumConverter
454        let enum_conv = converters::EnumConverter::new();
455        for kind in enum_conv.spec_kinds() {
456            assert!(
457                registered.contains(kind),
458                "EnumConverter::spec_kinds() contains '{}' but NOT registered",
459                kind
460            );
461        }
462
463        // Check ModuleConverter
464        let module = converters::ModuleConverter::new();
465        for kind in module.spec_kinds() {
466            assert!(
467                registered.contains(kind),
468                "ModuleConverter::spec_kinds() contains '{}' but NOT registered",
469                kind
470            );
471        }
472
473        // Check StmtConverter
474        let stmt = converters::StmtConverter::new();
475        for kind in stmt.spec_kinds() {
476            assert!(
477                registered.contains(kind),
478                "StmtConverter::spec_kinds() contains '{}' but NOT registered",
479                kind
480            );
481        }
482
483        // Check DuplicateConverter
484        let dup = converters::DuplicateConverter::new();
485        for kind in dup.spec_kinds() {
486            assert!(
487                registered.contains(kind),
488                "DuplicateConverter::spec_kinds() contains '{}' but NOT registered",
489                kind
490            );
491        }
492    }
493}
494
495#[cfg(test)]
496mod tests_pre_check {
497    use super::*;
498    use ryo_analysis::testing::ContextBuilder;
499    use ryo_source::pure::{
500        PureEnum, PureField, PureFields, PureFile, PureItem, PureStruct, PureType, PureVariant,
501        PureVis,
502    };
503
504    /// Create a simple PureFile with a struct
505    fn make_test_file_with_struct(struct_name: &str, fields: &[(&str, &str)]) -> PureFile {
506        let pure_fields = fields
507            .iter()
508            .map(|(name, ty)| PureField {
509                name: name.to_string(),
510                ty: PureType::Path(ty.to_string()),
511                attrs: vec![],
512                vis: PureVis::Public,
513            })
514            .collect();
515
516        PureFile {
517            attrs: vec![],
518            items: vec![PureItem::Struct(PureStruct {
519                name: struct_name.to_string(),
520                vis: PureVis::Public,
521                generics: Default::default(),
522                fields: PureFields::Named(pure_fields),
523                attrs: vec![],
524            })],
525        }
526    }
527
528    /// Create a simple PureFile with an enum
529    fn make_test_file_with_enum(enum_name: &str, variants: &[&str]) -> PureFile {
530        let pure_variants = variants
531            .iter()
532            .map(|name| PureVariant {
533                name: name.to_string(),
534                attrs: vec![],
535                fields: PureFields::Unit,
536                discriminant: None,
537            })
538            .collect();
539
540        PureFile {
541            attrs: vec![],
542            items: vec![PureItem::Enum(PureEnum {
543                name: enum_name.to_string(),
544                vis: PureVis::Public,
545                generics: Default::default(),
546                variants: pure_variants,
547                attrs: vec![],
548            })],
549        }
550    }
551
552    #[test]
553    fn test_pre_check_rename_with_symbol_id() {
554        use ryo_symbol::{SymbolKind, SymbolPath, SymbolRegistry};
555
556        let registry = MutationRegistry::new();
557        let file = make_test_file_with_struct("Config", &[("timeout", "u64")]);
558        let ctx = ContextBuilder::new()
559            .with_pure_file("src/lib.rs", file)
560            .build();
561
562        // Create a SymbolId (symbol_id is now required)
563        let mut symbol_registry = SymbolRegistry::new();
564        let path = SymbolPath::parse("test_crate::Config").unwrap();
565        let symbol_id = symbol_registry.register(path, SymbolKind::Struct).unwrap();
566
567        // Rename spec with required symbol_id
568        let spec = MutationSpec::Rename {
569            target: crate::executor::spec::MutationTargetSymbol::ById(symbol_id),
570            to: "Settings".into(),
571            scope: crate::executor::spec::Scope::Project,
572        };
573
574        // Pre-check is now a no-op for Rename (symbol_id is required, so no name-based lookup)
575        let result = registry.pre_check(&spec, &ctx);
576        assert!(
577            result.is_ok(),
578            "Pre-check should always pass for Rename with symbol_id: {:?}",
579            result
580        );
581    }
582
583    #[test]
584    fn test_pre_check_add_field_with_symbol_id() {
585        use ryo_symbol::{SymbolKind, SymbolPath, SymbolRegistry};
586
587        let mutation_registry = MutationRegistry::new();
588        let file = make_test_file_with_struct("Config", &[("timeout", "u64")]);
589
590        // Create a SymbolId (pre_check doesn't verify it exists in ctx)
591        let mut symbol_registry = SymbolRegistry::new();
592        let path = SymbolPath::parse("test_crate::Config").unwrap();
593        let symbol_id = symbol_registry.register(path, SymbolKind::Struct).unwrap();
594
595        let ctx = ContextBuilder::new()
596            .with_pure_file("src/lib.rs", file)
597            .build();
598
599        let spec = MutationSpec::AddField {
600            target: crate::executor::spec::MutationTargetSymbol::ById(symbol_id),
601            field_name: "name".into(),
602            field_type: "String".into(),
603            visibility: crate::executor::spec::Visibility::Pub,
604        };
605
606        // pre_check for AddField is now a no-op since symbol_id is required
607        let result = mutation_registry.pre_check(&spec, &ctx);
608        assert!(
609            result.is_ok(),
610            "Pre-check should always pass for AddField with required symbol_id: {:?}",
611            result
612        );
613    }
614
615    #[test]
616    fn test_pre_check_add_variant_always_passes() {
617        // AddVariant has required symbol_id, pre_check always passes
618        let registry = MutationRegistry::new();
619        let file = make_test_file_with_enum("Status", &["Active", "Inactive"]);
620        let ctx = ContextBuilder::new()
621            .with_pure_file("src/lib.rs", file)
622            .build();
623
624        // Get the enum's SymbolId from registry
625        let enum_id = ctx
626            .registry()
627            .iter()
628            .find(|(_, path)| path.name() == "Status")
629            .map(|(id, _)| id)
630            .expect("Status enum should exist");
631
632        let spec = MutationSpec::AddVariant {
633            target: crate::executor::spec::MutationTargetSymbol::ById(enum_id),
634            variant_name: "Pending".into(),
635            variant_kind: crate::executor::spec::VariantKind::Unit,
636        };
637
638        let result = registry.pre_check(&spec, &ctx);
639        assert!(
640            result.is_ok(),
641            "Pre-check should always pass for AddVariant with required symbol_id: {:?}",
642            result
643        );
644    }
645
646    #[test]
647    fn test_pre_check_add_derive_with_symbol_id() {
648        use ryo_symbol::{SymbolKind, SymbolPath, SymbolRegistry};
649
650        let registry = MutationRegistry::new();
651        let file = make_test_file_with_struct("Config", &[("timeout", "u64")]);
652        let ctx = ContextBuilder::new()
653            .with_pure_file("src/lib.rs", file)
654            .build();
655
656        // Create a SymbolId (symbol_id is now required)
657        let mut symbol_registry = SymbolRegistry::new();
658        let path = SymbolPath::parse("test_crate::Config").unwrap();
659        let symbol_id = symbol_registry.register(path, SymbolKind::Struct).unwrap();
660
661        let spec = MutationSpec::AddDerive {
662            target: crate::executor::spec::MutationTargetSymbol::ById(symbol_id),
663            derives: vec!["Debug".into(), "Clone".into()],
664        };
665
666        // Pre-check is now a no-op for AddDerive (symbol_id is required)
667        let result = registry.pre_check(&spec, &ctx);
668        assert!(
669            result.is_ok(),
670            "Pre-check should always pass for AddDerive with symbol_id: {:?}",
671            result
672        );
673    }
674
675    #[test]
676    fn test_pre_check_add_item_no_check_needed() {
677        let registry = MutationRegistry::new();
678        let file = make_test_file_with_struct("Config", &[]);
679        let ctx = ContextBuilder::new()
680            .with_pure_file("src/lib.rs", file)
681            .build();
682
683        // AddItem doesn't need pre-check (it adds new items)
684        let spec = MutationSpec::AddItem {
685            target: crate::executor::spec::MutationTargetSymbol::ByPath(Box::new(
686                crate::executor::spec::SymbolPath::parse("test_crate").unwrap(),
687            )),
688            content: "struct NewStruct {}".into(),
689            position: crate::executor::spec::InsertPosition::Bottom,
690        };
691
692        let result = registry.pre_check(&spec, &ctx);
693        assert!(result.is_ok(), "AddItem should not require pre-check");
694    }
695}