Skip to main content

ryo_mutations/
lib.rs

1//! ryo-mutations: Code transformation primitives for Rust source code
2//!
3//! This crate provides a unified interface for AST-level mutations with
4//! validation, serialization, and parallel execution support.
5//!
6//! ## Module Structure
7//!
8//! - `basic/` - Fundamental AST operations (add, remove, rename, visibility)
9//! - `refactor/` - Structural transformations (extract, inline, split, merge)
10//! - `idiom/` - Idiomatic Rust patterns (unwrap→?, loops→iterators, Default)
11//! - `clippy/` - Clippy lint integration
12//! - `analyzer/` - rust-analyzer LSP integration
13//! - `debugger/` - Debug logging utilities
14//!
15//! ## Validation
16//!
17//! Mutations can be validated before execution using [`Mutation::validate`].
18//! The validation result can be controlled by [`ValidationStrategy`]:
19//!
20//! - `AllowAll` - For FactionBoard/speculative execution (proceed regardless)
21//! - `BlockFatalOnly` - For speculative execution (block only fatal errors)
22//! - `BlockConflicts` - For sequential execution (block potential conflicts)
23//! - `Strict` - For debugging (block all issues)
24
25pub mod analyzer;
26pub mod basic;
27pub mod clippy;
28pub mod debugger;
29pub mod idiom;
30pub mod serializable;
31
32use ryo_source::pure::PureFile;
33use serde::{Deserialize, Serialize};
34use std::path::PathBuf;
35
36// ============================================================================
37// Validation Types
38// ============================================================================
39
40/// Validation severity level (ordered by severity)
41#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
42pub enum ValidationLevel {
43    /// Information only (no impact on execution)
44    Info,
45    /// Warning (may cause issues in parallel execution, but safe alone)
46    Warning,
47    /// Potential conflict with other mutations
48    Conflict,
49    /// Fatal error (mutation will definitely fail)
50    Fatal,
51}
52
53impl ValidationLevel {
54    pub fn is_fatal(&self) -> bool {
55        *self == ValidationLevel::Fatal
56    }
57
58    pub fn is_blocking(&self) -> bool {
59        *self >= ValidationLevel::Conflict
60    }
61}
62
63/// Validation issue code for categorization
64#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
65pub enum ValidationCode {
66    // === Fatal ===
67    /// Target symbol does not exist (Rename/Remove)
68    TargetNotFound,
69    /// Symbol already exists (Add)
70    DuplicateSymbol,
71    /// Generated code would fail syn parse
72    InvalidSyntax,
73    /// Required type/trait not found
74    MissingDependency,
75
76    // === Conflict ===
77    /// Symbol is referenced elsewhere (Remove may break references)
78    SymbolReferenced,
79    /// Another mutation targets the same symbol
80    SameSymbolTarget,
81    /// Impl block target type not found in file
82    ImplTargetNotFound,
83
84    // === Warning ===
85    /// Added import may be unused
86    PotentiallyUnusedImport,
87    /// Visibility may not be sufficient
88    VisibilityMismatch,
89    /// Naming convention violation
90    NamingConvention,
91
92    // === Info ===
93    /// No changes would be made (already applied, etc.)
94    NoOp,
95    /// Symbol has no references (safe to remove)
96    UnreferencedSymbol,
97}
98
99impl ValidationCode {
100    /// Get the default level for this code
101    pub fn default_level(&self) -> ValidationLevel {
102        match self {
103            // Fatal
104            ValidationCode::TargetNotFound
105            | ValidationCode::DuplicateSymbol
106            | ValidationCode::InvalidSyntax
107            | ValidationCode::MissingDependency => ValidationLevel::Fatal,
108
109            // Conflict
110            ValidationCode::SymbolReferenced
111            | ValidationCode::SameSymbolTarget
112            | ValidationCode::ImplTargetNotFound => ValidationLevel::Conflict,
113
114            // Warning
115            ValidationCode::PotentiallyUnusedImport
116            | ValidationCode::VisibilityMismatch
117            | ValidationCode::NamingConvention => ValidationLevel::Warning,
118
119            // Info
120            ValidationCode::NoOp | ValidationCode::UnreferencedSymbol => ValidationLevel::Info,
121        }
122    }
123
124    /// Get a user-friendly description of this error code
125    pub fn user_friendly_message(&self) -> &'static str {
126        match self {
127            // Fatal
128            ValidationCode::TargetNotFound => {
129                "The symbol you're trying to modify doesn't exist in the codebase"
130            }
131            ValidationCode::DuplicateSymbol => "A symbol with this name already exists",
132            ValidationCode::InvalidSyntax => "The transformation would produce invalid Rust code",
133            ValidationCode::MissingDependency => "A required type or trait is not available",
134
135            // Conflict
136            ValidationCode::SymbolReferenced => {
137                "This symbol is used elsewhere; removing it may break other code"
138            }
139            ValidationCode::SameSymbolTarget => "Multiple mutations are targeting the same symbol",
140            ValidationCode::ImplTargetNotFound => "The type for this impl block was not found",
141
142            // Warning
143            ValidationCode::PotentiallyUnusedImport => "This import might not be used anywhere",
144            ValidationCode::VisibilityMismatch => {
145                "The visibility might not be sufficient for intended usage"
146            }
147            ValidationCode::NamingConvention => "The name doesn't follow Rust naming conventions",
148
149            // Info
150            ValidationCode::NoOp => "No changes are needed (already in desired state)",
151            ValidationCode::UnreferencedSymbol => "This symbol has no references (safe to remove)",
152        }
153    }
154
155    /// Get a suggestion for how to fix this issue
156    pub fn suggestion(&self) -> Option<&'static str> {
157        match self {
158            ValidationCode::TargetNotFound => Some(
159                "Check the symbol name for typos, or use 'ryo discover' to find available symbols"
160            ),
161            ValidationCode::DuplicateSymbol => Some(
162                "Choose a different name, or remove the existing symbol first"
163            ),
164            ValidationCode::InvalidSyntax => Some(
165                "Review the transformation parameters; ensure the generated code is valid Rust"
166            ),
167            ValidationCode::MissingDependency => Some(
168                "Add the required dependency to Cargo.toml, or import the type/trait"
169            ),
170            ValidationCode::SymbolReferenced => Some(
171                "Update all references before removing, or use 'ryo graph cascade' to see impact"
172            ),
173            ValidationCode::SameSymbolTarget => Some(
174                "Merge mutations into a single operation, or execute them sequentially"
175            ),
176            ValidationCode::ImplTargetNotFound => Some(
177                "Ensure the target type is defined in the same file, or specify the full path"
178            ),
179            ValidationCode::NamingConvention => Some(
180                "Use snake_case for functions/variables, PascalCase for types, SCREAMING_CASE for constants"
181            ),
182            // No suggestions needed for these
183            ValidationCode::PotentiallyUnusedImport => None,
184            ValidationCode::VisibilityMismatch => None,
185            ValidationCode::NoOp => None,
186            ValidationCode::UnreferencedSymbol => None,
187        }
188    }
189
190    /// Get an example of correct usage (for common errors)
191    pub fn example(&self) -> Option<&'static str> {
192        match self {
193            ValidationCode::TargetNotFound => Some(
194                r#"Example: { "intent": "RenameIdent", "from": "existing_fn", "to": "new_name" }"#,
195            ),
196            ValidationCode::DuplicateSymbol => {
197                Some(r#"Example: First rename or remove 'foo', then add the new symbol"#)
198            }
199            ValidationCode::NamingConvention => {
200                Some(r#"Examples: fn my_function(), struct MyStruct, const MAX_SIZE"#)
201            }
202            _ => None,
203        }
204    }
205}
206
207/// A validation issue found during pre-execution validation
208#[derive(Debug, Clone, Serialize, Deserialize)]
209pub struct ValidationIssue {
210    /// Severity level
211    pub level: ValidationLevel,
212    /// Issue code for categorization
213    pub code: ValidationCode,
214    /// Human-readable message
215    pub message: String,
216    /// Affected symbol name (if applicable)
217    pub affected_symbol: Option<String>,
218}
219
220impl ValidationIssue {
221    /// Create a new validation issue
222    pub fn new(code: ValidationCode, message: impl Into<String>) -> Self {
223        Self {
224            level: code.default_level(),
225            code,
226            message: message.into(),
227            affected_symbol: None,
228        }
229    }
230
231    /// Create with a custom level (override default)
232    pub fn with_level(mut self, level: ValidationLevel) -> Self {
233        self.level = level;
234        self
235    }
236
237    /// Add affected symbol information
238    pub fn with_symbol(mut self, symbol: impl Into<String>) -> Self {
239        self.affected_symbol = Some(symbol.into());
240        self
241    }
242
243    // Convenience constructors for common issues
244    pub fn target_not_found(symbol: &str) -> Self {
245        Self::new(
246            ValidationCode::TargetNotFound,
247            format!("Target symbol '{}' not found", symbol),
248        )
249        .with_symbol(symbol)
250    }
251
252    pub fn duplicate_symbol(symbol: &str) -> Self {
253        Self::new(
254            ValidationCode::DuplicateSymbol,
255            format!("Symbol '{}' already exists", symbol),
256        )
257        .with_symbol(symbol)
258    }
259
260    pub fn symbol_referenced(symbol: &str, ref_count: usize) -> Self {
261        Self::new(
262            ValidationCode::SymbolReferenced,
263            format!(
264                "Symbol '{}' is referenced {} time(s); removal may break code",
265                symbol, ref_count
266            ),
267        )
268        .with_symbol(symbol)
269    }
270
271    pub fn no_op(reason: &str) -> Self {
272        Self::new(ValidationCode::NoOp, reason.to_string())
273    }
274
275    /// Format this issue as a user-friendly message with suggestions
276    pub fn format_user_friendly(&self) -> String {
277        let mut output = String::new();
278
279        // Level indicator
280        let level_indicator = match self.level {
281            ValidationLevel::Fatal => "[ERROR]",
282            ValidationLevel::Conflict => "[CONFLICT]",
283            ValidationLevel::Warning => "[WARNING]",
284            ValidationLevel::Info => "[INFO]",
285        };
286
287        // Main message
288        output.push_str(&format!(
289            "{} {}\n",
290            level_indicator,
291            self.code.user_friendly_message()
292        ));
293
294        // Technical details
295        if let Some(ref symbol) = self.affected_symbol {
296            output.push_str(&format!("  Symbol: {}\n", symbol));
297        }
298        if !self.message.is_empty() && self.message != self.code.user_friendly_message() {
299            output.push_str(&format!("  Detail: {}\n", self.message));
300        }
301
302        // Suggestion
303        if let Some(suggestion) = self.code.suggestion() {
304            output.push_str(&format!("  Suggestion: {}\n", suggestion));
305        }
306
307        // Example
308        if let Some(example) = self.code.example() {
309            output.push_str(&format!("  {}\n", example));
310        }
311
312        output
313    }
314}
315
316impl std::fmt::Display for ValidationIssue {
317    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
318        write!(f, "{}", self.format_user_friendly())
319    }
320}
321
322/// Validation result containing all issues
323#[derive(Debug, Clone, Default, Serialize, Deserialize)]
324pub struct ValidationResult {
325    pub issues: Vec<ValidationIssue>,
326}
327
328impl ValidationResult {
329    pub fn new() -> Self {
330        Self { issues: Vec::new() }
331    }
332
333    pub fn ok() -> Self {
334        Self::new()
335    }
336
337    pub fn with_issue(mut self, issue: ValidationIssue) -> Self {
338        self.issues.push(issue);
339        self
340    }
341
342    pub fn add(&mut self, issue: ValidationIssue) {
343        self.issues.push(issue);
344    }
345
346    pub fn is_ok(&self) -> bool {
347        self.issues.is_empty()
348    }
349
350    pub fn has_fatal(&self) -> bool {
351        self.issues
352            .iter()
353            .any(|i| i.level == ValidationLevel::Fatal)
354    }
355
356    pub fn has_conflicts(&self) -> bool {
357        self.issues
358            .iter()
359            .any(|i| i.level >= ValidationLevel::Conflict)
360    }
361
362    pub fn has_warnings(&self) -> bool {
363        self.issues
364            .iter()
365            .any(|i| i.level >= ValidationLevel::Warning)
366    }
367
368    pub fn max_level(&self) -> Option<ValidationLevel> {
369        self.issues.iter().map(|i| i.level).max()
370    }
371
372    pub fn by_level(&self, level: ValidationLevel) -> Vec<&ValidationIssue> {
373        self.issues.iter().filter(|i| i.level == level).collect()
374    }
375
376    /// Format all issues as user-friendly messages
377    pub fn format_user_friendly(&self) -> String {
378        if self.issues.is_empty() {
379            return String::new();
380        }
381
382        let mut output = String::new();
383
384        // Group by level for better readability
385        let fatal: Vec<_> = self.by_level(ValidationLevel::Fatal);
386        let conflicts: Vec<_> = self.by_level(ValidationLevel::Conflict);
387        let warnings: Vec<_> = self.by_level(ValidationLevel::Warning);
388        let info: Vec<_> = self.by_level(ValidationLevel::Info);
389
390        if !fatal.is_empty() {
391            output.push_str(&format!("=== {} Error(s) ===\n", fatal.len()));
392            for issue in fatal {
393                output.push_str(&issue.format_user_friendly());
394                output.push('\n');
395            }
396        }
397
398        if !conflicts.is_empty() {
399            output.push_str(&format!("=== {} Conflict(s) ===\n", conflicts.len()));
400            for issue in conflicts {
401                output.push_str(&issue.format_user_friendly());
402                output.push('\n');
403            }
404        }
405
406        if !warnings.is_empty() {
407            output.push_str(&format!("=== {} Warning(s) ===\n", warnings.len()));
408            for issue in warnings {
409                output.push_str(&issue.format_user_friendly());
410                output.push('\n');
411            }
412        }
413
414        if !info.is_empty() {
415            output.push_str(&format!("=== {} Info ===\n", info.len()));
416            for issue in info {
417                output.push_str(&issue.format_user_friendly());
418                output.push('\n');
419            }
420        }
421
422        output
423    }
424}
425
426impl std::fmt::Display for ValidationResult {
427    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
428        write!(f, "{}", self.format_user_friendly())
429    }
430}
431
432/// Strategy for handling validation results
433///
434/// Different execution contexts require different validation strictness:
435/// - FactionBoard: Each agent operates in isolation, conflicts resolved at compose time
436/// - Speculative: Optimistic execution, rollback on conflict
437/// - Sequential: Traditional execution, block on potential conflicts
438#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
439pub enum ValidationStrategy {
440    /// Allow all issues (for FactionBoard / fully speculative execution)
441    /// Conflicts will be detected at compose/merge time
442    AllowAll,
443
444    /// Block only fatal errors (for speculative execution)
445    /// Conflicts are allowed, will be resolved at merge time
446    #[default]
447    BlockFatalOnly,
448
449    /// Block conflicts and fatal errors (for sequential execution)
450    /// Only warnings and info are allowed
451    BlockConflicts,
452
453    /// Block all issues including warnings (for debugging / strict mode)
454    Strict,
455}
456
457impl ValidationStrategy {
458    /// Check if execution can proceed given the validation result
459    pub fn can_proceed(&self, result: &ValidationResult) -> bool {
460        match self {
461            ValidationStrategy::AllowAll => true,
462            ValidationStrategy::BlockFatalOnly => !result.has_fatal(),
463            ValidationStrategy::BlockConflicts => !result.has_conflicts(),
464            ValidationStrategy::Strict => result.is_ok(),
465        }
466    }
467
468    /// Get the minimum blocking level for this strategy
469    pub fn blocking_level(&self) -> Option<ValidationLevel> {
470        match self {
471            ValidationStrategy::AllowAll => None,
472            ValidationStrategy::BlockFatalOnly => Some(ValidationLevel::Fatal),
473            ValidationStrategy::BlockConflicts => Some(ValidationLevel::Conflict),
474            ValidationStrategy::Strict => Some(ValidationLevel::Info),
475        }
476    }
477}
478
479// Re-export all mutations for convenience
480// Basic mutations
481pub use basic::{
482    AddConstMutation, AddDeriveMutation, AddEnumMutation, AddFieldMutation, AddFunctionMutation,
483    AddImplMutation, AddItemMutation, AddMatchArmMutation, AddMethodMutation, AddPureItemsMutation,
484    AddStructLiteralFieldMutation, AddStructMutation, AddTypeAliasMutation, AddUseMutation,
485    AddVariantMutation, ChangeVisibilityMutation, CreateModMutation, EnumToTraitMutation,
486    EnumToTraitStrategy, ExtractTraitMutation, FieldInfo, InlineTraitMutation, MatchHandling,
487    MoveItemMutation, RemoveConstMutation, RemoveDeriveMutation, RemoveEnumMutation,
488    RemoveFieldMutation, RemoveFunctionMutation, RemoveImplMutation, RemoveItemMutation,
489    RemoveMatchArmMutation, RemoveMethodMutation, RemoveModMutation,
490    RemoveStructLiteralFieldMutation, RemoveStructMutation, RemoveTraitMutation,
491    RemoveTypeAliasMutation, RemoveUseMutation, RemoveVariantMutation, RenameMutation,
492    ReplaceMatchArmMutation, VariantInfo,
493};
494
495// Refactor mutations
496// (Currently empty - use basic mutations for refactoring operations)
497
498// Idiom mutations
499pub use idiom::{
500    // Tier 1: Clippy high-frequency
501    AssignOpMutation,
502    BoolSimplifyMutation,
503    CloneOnCopyMutation,
504    CollapsibleIfMutation,
505    ComparisonToMethodMutation,
506    // Tier 3: Code generation (Design patterns)
507    DefaultMutation,
508    DeriveDefaultMutation,
509    FilterNextMutation,
510    // Tier 2: Pattern transformations
511    FindDuplicateExpressions,
512    IntroduceVariableMutation,
513    // Tier 4: Performance/Safety
514    LockScopeMutation,
515    LoopPattern,
516    LoopToIteratorMutation,
517    ManualMapMutation,
518    MapUnwrapOrMutation,
519    MatchToIfLetMutation,
520    NoOpArmToTodoMutation,
521    OrganizeImportsMutation,
522    RedundantClosureMutation,
523    UnwrapToQuestionMutation,
524    UseAtomicMutation,
525    UseRwLockMutation,
526};
527
528// Detection trait and types
529pub use idiom::detect::{
530    create_default_registry, Detect, DetectCategory, DetectLocation, DetectOperation,
531    DetectOpportunity, DetectRegistry,
532};
533
534// Statement-level mutations (from basic::stmt)
535pub use basic::stmt::{
536    InsertPosition, InsertStatementMutation, RemoveStatementMutation, ReplaceExprAtMutation,
537    ReplaceExprMutation, ReplaceStatementMutation, WrapExprMutation,
538};
539
540// Debugger mutations
541pub use debugger::{
542    DbgWrapMutation, DebugMarker, DebugSession, InsertInspectMutation, RemovalTarget,
543    RemoveDebugLogsMutation, MARKER_PREFIX,
544};
545
546// Serializable mutation representation
547pub use serializable::{SerializableMutation, ToSerializable};
548
549/// Result of applying a mutation
550#[derive(Debug, Clone, Serialize, Deserialize)]
551pub struct MutationResult {
552    pub mutation_type: String,
553    pub changes: usize,
554    pub description: String,
555}
556
557/// A mutation that can be applied to AST.
558///
559/// # Execution APIs
560///
561/// - **`ASTRegApply::apply_to_registry()`** (V2, preferred): Registry-based execution
562///   via `ASTMutationContext`. Defined in `ryo-executor` to avoid circular dependencies.
563///
564/// - **`apply()`** (Legacy, deprecated): File-based execution via `&mut PureFile`.
565///   Will be removed in future versions.
566///
567/// # Migration Guide
568///
569/// New mutations should implement `ASTRegApply` trait in `ryo-executor::engine`.
570/// See `ryo-executor::engine::impls` for implementation examples.
571pub trait Mutation: Send + Sync {
572    /// Validate the mutation before applying
573    ///
574    /// Returns validation issues that may indicate:
575    /// - Fatal errors (mutation will fail)
576    /// - Conflicts (may conflict with parallel mutations)
577    /// - Warnings (potential issues)
578    /// - Info (informational, no impact)
579    ///
580    /// Default implementation returns no issues (always valid).
581    fn validate(&self, _file: &PureFile) -> ValidationResult {
582        ValidationResult::ok()
583    }
584
585    /// Check if this mutation can proceed with the given strategy
586    fn can_proceed(&self, file: &PureFile, strategy: ValidationStrategy) -> bool {
587        strategy.can_proceed(&self.validate(file))
588    }
589
590    /// Get a description of this mutation
591    fn describe(&self) -> String;
592
593    /// Get the mutation type name
594    fn mutation_type(&self) -> &'static str;
595
596    /// Clone this mutation into a new Box
597    fn box_clone(&self) -> Box<dyn Mutation>;
598}
599
600/// Clone a boxed mutation
601pub fn clone_mutation(mutation: &dyn Mutation) -> Box<dyn Mutation> {
602    mutation.box_clone()
603}
604
605/// A collection of mutations to apply
606#[derive(Debug)]
607pub struct MutationBatch {
608    pub mutations: Vec<Box<dyn Mutation>>,
609}
610
611// Manual Debug impl because Box<dyn Mutation> doesn't implement Debug
612impl std::fmt::Debug for Box<dyn Mutation> {
613    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
614        write!(f, "Mutation({})", self.mutation_type())
615    }
616}
617
618impl MutationBatch {
619    pub fn new() -> Self {
620        Self {
621            mutations: Vec::new(),
622        }
623    }
624
625    pub fn with_mutation<M: Mutation + 'static>(mut self, mutation: M) -> Self {
626        self.mutations.push(Box::new(mutation));
627        self
628    }
629}
630
631impl Default for MutationBatch {
632    fn default() -> Self {
633        Self::new()
634    }
635}
636
637// Suppress unused warning when parallel feature is not enabled
638#[allow(dead_code)]
639fn _use_path_buf(_: PathBuf) {}