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