1pub 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#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
42pub enum ValidationLevel {
43 Info,
45 Warning,
47 Conflict,
49 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
65pub enum ValidationCode {
66 TargetNotFound,
69 DuplicateSymbol,
71 InvalidSyntax,
73 MissingDependency,
75
76 SymbolReferenced,
79 SameSymbolTarget,
81 ImplTargetNotFound,
83
84 PotentiallyUnusedImport,
87 VisibilityMismatch,
89 NamingConvention,
91
92 NoOp,
95 UnreferencedSymbol,
97}
98
99impl ValidationCode {
100 pub fn default_level(&self) -> ValidationLevel {
102 match self {
103 ValidationCode::TargetNotFound
105 | ValidationCode::DuplicateSymbol
106 | ValidationCode::InvalidSyntax
107 | ValidationCode::MissingDependency => ValidationLevel::Fatal,
108
109 ValidationCode::SymbolReferenced
111 | ValidationCode::SameSymbolTarget
112 | ValidationCode::ImplTargetNotFound => ValidationLevel::Conflict,
113
114 ValidationCode::PotentiallyUnusedImport
116 | ValidationCode::VisibilityMismatch
117 | ValidationCode::NamingConvention => ValidationLevel::Warning,
118
119 ValidationCode::NoOp | ValidationCode::UnreferencedSymbol => ValidationLevel::Info,
121 }
122 }
123
124 pub fn user_friendly_message(&self) -> &'static str {
126 match self {
127 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 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 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 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 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 ValidationCode::PotentiallyUnusedImport => None,
184 ValidationCode::VisibilityMismatch => None,
185 ValidationCode::NoOp => None,
186 ValidationCode::UnreferencedSymbol => None,
187 }
188 }
189
190 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#[derive(Debug, Clone, Serialize, Deserialize)]
209pub struct ValidationIssue {
210 pub level: ValidationLevel,
212 pub code: ValidationCode,
214 pub message: String,
216 pub affected_symbol: Option<String>,
218}
219
220impl ValidationIssue {
221 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 pub fn with_level(mut self, level: ValidationLevel) -> Self {
233 self.level = level;
234 self
235 }
236
237 pub fn with_symbol(mut self, symbol: impl Into<String>) -> Self {
239 self.affected_symbol = Some(symbol.into());
240 self
241 }
242
243 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 pub fn format_user_friendly(&self) -> String {
277 let mut output = String::new();
278
279 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 output.push_str(&format!(
289 "{} {}\n",
290 level_indicator,
291 self.code.user_friendly_message()
292 ));
293
294 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 if let Some(suggestion) = self.code.suggestion() {
304 output.push_str(&format!(" Suggestion: {}\n", suggestion));
305 }
306
307 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#[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 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
439pub enum ValidationStrategy {
440 AllowAll,
443
444 #[default]
447 BlockFatalOnly,
448
449 BlockConflicts,
452
453 Strict,
455}
456
457impl ValidationStrategy {
458 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 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
479pub 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
495pub use idiom::{
500 AssignOpMutation,
502 BoolSimplifyMutation,
503 CloneOnCopyMutation,
504 CollapsibleIfMutation,
505 ComparisonToMethodMutation,
506 DefaultMutation,
508 DeriveDefaultMutation,
509 FilterNextMutation,
510 FindDuplicateExpressions,
512 IntroduceVariableMutation,
513 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
528pub use idiom::detect::{
530 create_default_registry, Detect, DetectCategory, DetectLocation, DetectOperation,
531 DetectOpportunity, DetectRegistry,
532};
533
534pub use basic::stmt::{
536 InsertPosition, InsertStatementMutation, RemoveStatementMutation, ReplaceExprAtMutation,
537 ReplaceExprMutation, ReplaceStatementMutation, WrapExprMutation,
538};
539
540pub use debugger::{
542 DbgWrapMutation, DebugMarker, DebugSession, InsertInspectMutation, RemovalTarget,
543 RemoveDebugLogsMutation, MARKER_PREFIX,
544};
545
546pub use serializable::{SerializableMutation, ToSerializable};
548
549#[derive(Debug, Clone, Serialize, Deserialize)]
551pub struct MutationResult {
552 pub mutation_type: String,
553 pub changes: usize,
554 pub description: String,
555}
556
557pub trait Mutation: Send + Sync {
572 fn validate(&self, _file: &PureFile) -> ValidationResult {
582 ValidationResult::ok()
583 }
584
585 fn can_proceed(&self, file: &PureFile, strategy: ValidationStrategy) -> bool {
587 strategy.can_proceed(&self.validate(file))
588 }
589
590 fn describe(&self) -> String;
592
593 fn mutation_type(&self) -> &'static str;
595
596 fn box_clone(&self) -> Box<dyn Mutation>;
598}
599
600pub fn clone_mutation(mutation: &dyn Mutation) -> Box<dyn Mutation> {
602 mutation.box_clone()
603}
604
605#[derive(Debug)]
607pub struct MutationBatch {
608 pub mutations: Vec<Box<dyn Mutation>>,
609}
610
611impl 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#[allow(dead_code)]
639fn _use_path_buf(_: PathBuf) {}