mago_fixer/
lib.rs

1use core::ops::Range;
2
3use serde::Deserialize;
4use serde::Serialize;
5use strum::Display;
6
7/// Represents a single change or difference between two versions of a string.
8///
9/// A `Change` indicates how a specific portion of the original text has been modified,
10/// whether by being left unchanged, having new content inserted, or having some content deleted.
11/// It forms the core building block of representing modifications between two versions of text.
12#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize, PartialOrd, Ord)]
13#[serde(tag = "type", content = "value")]
14pub enum Change {
15    /// Represents a section of text that remains unchanged between the original and the modified versions.
16    Unchanged(String),
17
18    /// Represents text that has been added in the modified version.
19    Inserted(String),
20
21    /// Represents text that has been removed in the modified version.
22    Deleted(String),
23}
24
25/// Represents a collection of differences (changes) between the original and modified versions of a string.
26///
27/// A `ChangeSet` stores the sequence of changes that have occurred between two versions of a code snippet or text.
28/// This struct provides the necessary data to reconstruct both the original and modified versions from
29/// the list of changes. It serves as a foundational structure for comparing two versions of content.
30#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
31pub struct ChangeSet {
32    /// A list of changes, where each `Change` represents an insertion, deletion, or unchanged portion of the text.
33    ///
34    /// These changes, when applied in sequence, reconstruct either the original text or the modified text,
35    /// depending on whether insertions or deletions are ignored.
36    changes: Vec<Change>,
37}
38
39/// Represents the safety classifications of a code fix operation.
40#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Serialize, Deserialize, PartialOrd, Ord, Display)]
41#[serde(tag = "type", content = "value")]
42pub enum SafetyClassification {
43    /// Safe operations that are unlikely to cause issues.
44    Safe,
45    /// Operations that might cause issues under certain circumstances.
46    PotentiallyUnsafe,
47    /// Operations that are known to be unsafe.
48    Unsafe,
49}
50
51/// Represents an individual operation in a code fix plan.
52///
53/// A `FixOperation` can perform various types of modifications on a piece of text,
54/// such as inserting new content, replacing existing content, or deleting parts of the content.
55/// Each operation is associated with a safety classification that indicates how safe it is to apply
56/// the operation without causing unintended side effects.
57#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
58#[serde(tag = "type", content = "value")]
59pub enum FixOperation {
60    /// Inserts new text at a specified position within the content.
61    Insert {
62        /// The position (in bytes) where the new text will be inserted.
63        offset: u32,
64        /// The text to be inserted.
65        text: String,
66        /// The safety classification of this operation. It indicates how safe it is to apply the insertion.
67        safety_classification: SafetyClassification,
68    },
69
70    /// Replaces text in the specified range with new content.
71    Replace {
72        /// The range of text to be replaced, specified by start and end byte indices.
73        range: Range<u32>,
74        /// The new text that will replace the text within the given range.
75        text: String,
76        /// The safety classification of this operation.
77        safety_classification: SafetyClassification,
78    },
79
80    /// Deletes text within a specified range.
81    Delete {
82        /// The range of text to be deleted, specified by start and end byte indices.
83        range: Range<u32>,
84        /// The safety classification of this operation.
85        safety_classification: SafetyClassification,
86    },
87}
88
89/// Represents a sequence of code fix operations to be applied to a piece of content.
90///
91/// A `FixPlan` contains multiple operations that describe how to modify a string of code.
92/// The operations can include inserting new content, replacing old content, or deleting
93/// unwanted parts. The operations are ordered and will be applied sequentially to the content.
94#[derive(Default, Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
95pub struct FixPlan {
96    /// A SET of `FixOperation` instances that describe the specific changes to be made.
97    operations: Vec<FixOperation>,
98}
99
100impl ChangeSet {
101    /// Creates a new `ChangeSet` instance from a vector of `Change` instances.
102    ///
103    /// # Example
104    ///
105    /// ```rust
106    /// use mago_fixer::{Change, ChangeSet};
107    ///
108    /// let changes = vec![
109    ///     Change::Unchanged("Hello".to_string()),
110    ///     Change::Deleted(" World".to_string()),
111    ///     Change::Inserted(" Rustaceans".to_string()),
112    /// ];
113    ///
114    /// let change_set = ChangeSet::new(changes);
115    ///
116    /// assert_eq!(change_set.get_original(), "Hello World");
117    /// ```
118    pub fn new(changes: Vec<Change>) -> Self {
119        Self { changes }
120    }
121
122    /// Creates a new `ChangeSet` instance from an iterator of `Change` instances.
123    ///
124    /// # Example
125    ///
126    /// ```rust
127    /// use mago_fixer::{Change, ChangeSet};
128    ///
129    /// let changes = vec![
130    ///     Change::Unchanged("Hello".to_string()),
131    ///     Change::Deleted(" World".to_string()),
132    ///     Change::Inserted(" Rustaceans".to_string()),
133    /// ];
134    ///
135    /// let change_set = ChangeSet::from(changes);
136    /// ```
137    ///
138    /// # Parameters
139    ///
140    /// - `changes`: An iterator of `Change` instances.
141    ///
142    /// # Returns
143    ///
144    /// A new `ChangeSet` instance.
145    pub fn from(changes: impl IntoIterator<Item = Change>) -> Self {
146        Self { changes: changes.into_iter().collect() }
147    }
148
149    /// Reconstructs the original content from the list of changes.
150    ///
151    /// This method iterates over the `changes` vector and collects all the `Deleted` and `Unchanged`
152    /// parts, effectively ignoring any `Inserted` text. The result is a string identical to the original content
153    /// before any fix was applied.
154    ///
155    /// # Returns
156    ///
157    /// A `String` containing the original content.
158    ///
159    /// # Example
160    ///
161    /// ```rust
162    /// use mago_fixer::{Change, ChangeSet};
163    ///
164    /// let changes = vec![
165    ///     Change::Unchanged("Hello".to_string()),
166    ///     Change::Deleted(" World".to_string()),
167    ///     Change::Inserted(" Rustaceans".to_string()),
168    /// ];
169    ///
170    /// let change_set = ChangeSet::new(changes);
171    ///
172    /// assert_eq!(change_set.get_original(), "Hello World");
173    /// ```
174    #[inline]
175    pub fn get_original(&self) -> String {
176        let mut result = String::new();
177        for change in &self.changes {
178            match change {
179                Change::Deleted(text) => result.push_str(text),
180                Change::Unchanged(text) => result.push_str(text),
181                Change::Inserted(_) => {} // Ignore inserted text
182            }
183        }
184
185        result
186    }
187
188    /// Reconstructs the fixed content from the changes.
189    ///
190    /// This method iterates over the `changes` vector and collects all the `Inserted` and `Unchanged`
191    /// parts, effectively ignoring any `Deleted` text. The result is a string representing the content
192    /// after all fix has been applied.
193    ///
194    /// # Returns
195    ///
196    /// A `String` containing the fixed content.
197    ///
198    /// # Example
199    ///
200    /// ```rust
201    /// use mago_fixer::{Change, ChangeSet};
202    ///
203    /// let changes = vec![
204    ///     Change::Unchanged("Hello".to_string()),
205    ///     Change::Deleted(" World".to_string()),
206    ///     Change::Inserted(" Rustaceans".to_string()),
207    /// ];
208    ///
209    /// let change_set = ChangeSet::new(changes);
210    ///
211    /// assert_eq!(change_set.get_fixed(), "Hello Rustaceans");
212    /// ```
213    #[inline]
214    pub fn get_fixed(&self) -> String {
215        let mut result = String::new();
216        for change in &self.changes {
217            match change {
218                Change::Deleted(_) => {} // Ignore deleted text
219                Change::Unchanged(text) => result.push_str(text),
220                Change::Inserted(text) => result.push_str(text),
221            }
222        }
223        result
224    }
225
226    /// Returns the number of changes in the sequence.
227    pub fn len(&self) -> usize {
228        self.changes.len()
229    }
230
231    /// Returns `true` if the sequence contains no changes.
232    pub fn is_empty(&self) -> bool {
233        self.changes.is_empty()
234    }
235
236    /// Returns a reference to the changes in the sequence.
237    pub fn iter(&self) -> impl Iterator<Item = &Change> {
238        self.changes.iter()
239    }
240
241    /// Returns a mutable reference to the changes in the sequence.
242    pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut Change> {
243        self.changes.iter_mut()
244    }
245}
246
247impl FixOperation {
248    pub fn get_safety_classification(&self) -> SafetyClassification {
249        match self {
250            FixOperation::Insert { safety_classification, .. } => *safety_classification,
251            FixOperation::Replace { safety_classification, .. } => *safety_classification,
252            FixOperation::Delete { safety_classification, .. } => *safety_classification,
253        }
254    }
255}
256
257impl FixPlan {
258    /// Creates a new, empty `FixPlan` instance.
259    ///
260    /// This function initializes a `FixPlan` with no operations. You can use methods like
261    /// `insert`, `replace`, and `delete` to add specific operations to the plan.
262    ///
263    /// # Returns
264    /// A new `FixPlan` instance with no operations.
265    pub fn new() -> Self {
266        Self { operations: Vec::default() }
267    }
268
269    /// Creates a new `FixPlan` instance from a vector of `FixOperation` instances.
270    pub fn from_operations(operations: Vec<FixOperation>) -> Self {
271        Self { operations }
272    }
273
274    /// Adds a custom `FixOperation` to the plan.
275    ///
276    /// This method allows you to manually add a fix operation to the plan.
277    ///
278    /// # Arguments
279    ///
280    /// * `operation` - The operation to add, which can be an insertion, replacement, or deletion.
281    ///
282    /// # Returns
283    ///
284    /// The updated `FixPlan` instance with the new operation added.
285    pub fn operation(&mut self, operation: FixOperation) {
286        self.operations.push(operation);
287    }
288
289    /// Adds an insertion operation to the plan.
290    ///
291    /// This method creates and adds an `Insert` operation to the plan, specifying
292    /// where to insert the text and the text content itself.
293    ///
294    /// # Arguments
295    ///
296    /// * `offset` - The position at which the new text will be inserted.
297    /// * `text` - The content to insert.
298    /// * `safety` - The safety classification of this insertion.
299    ///
300    /// # Returns
301    ///
302    /// The updated `FixPlan` instance.
303    pub fn insert(&mut self, offset: u32, text: impl Into<String>, safety: SafetyClassification) {
304        self.operation(FixOperation::Insert { offset, text: text.into(), safety_classification: safety })
305    }
306
307    /// Adds a replacement operation to the plan.
308    ///
309    /// This method creates and adds a `Replace` operation to the plan, specifying
310    /// the range of text to be replaced and the new text to be inserted.
311    ///
312    /// # Arguments
313    ///
314    /// * `range` - The range of text to replace.
315    /// * `text` - The new content to insert.
316    /// * `safety` - The safety classification of this replacement.
317    ///
318    /// # Returns
319    ///
320    /// The updated `FixPlan` instance.
321    pub fn replace(&mut self, range: Range<u32>, text: impl Into<String>, safety: SafetyClassification) {
322        self.operation(FixOperation::Replace { range, text: text.into(), safety_classification: safety })
323    }
324
325    /// Adds a deletion operation to the plan.
326    ///
327    /// This method creates and adds a `Delete` operation to the plan, specifying
328    /// the range of text to be deleted.
329    ///
330    /// # Arguments
331    ///
332    /// * `range` - The range of text to delete.
333    /// * `safety` - The safety classification of this deletion.
334    ///
335    /// # Returns
336    ///
337    /// The updated `FixPlan` instance.
338    pub fn delete(&mut self, range: Range<u32>, safety: SafetyClassification) {
339        self.operation(FixOperation::Delete { range, safety_classification: safety })
340    }
341
342    /// Merges another `FixPlan` into this one.
343    ///
344    /// This method appends all the operations from another `FixPlan` to the end
345    /// of the current one, effectively combining two sequences of code fixes into one.
346    ///
347    /// # Arguments
348    ///
349    /// * `other` - The other `FixPlan` to merge.
350    pub fn merge(&mut self, other: FixPlan) {
351        for op in other.operations {
352            self.operation(op);
353        }
354    }
355
356    /// Returns a reference to the operations in the plan.
357    pub fn get_operations(&self) -> &Vec<FixOperation> {
358        &self.operations
359    }
360
361    /// Takes ownership of the operations in the plan.
362    ///
363    /// This method consumes the `FixPlan` and returns the list of operations it contains.
364    pub fn take_operations(self) -> Vec<FixOperation> {
365        self.operations
366    }
367
368    /// Determines the minimum safety classification across all operations in the plan.
369    ///
370    /// This function scans the safety classifications of all the operations in the plan and
371    /// returns the lowest (most restrictive) safety classification. This can be used to determine
372    /// whether the entire plan is safe to apply based on the user's preferred safety threshold.
373    ///
374    /// # Returns
375    ///
376    /// The minimum `SafetyClassification` of all operations.
377    #[inline]
378    pub fn get_minimum_safety_classification(&self) -> SafetyClassification {
379        self.operations
380            .iter()
381            .map(|op| match op {
382                FixOperation::Insert { safety_classification, .. } => *safety_classification,
383                FixOperation::Replace { safety_classification, .. } => *safety_classification,
384                FixOperation::Delete { safety_classification, .. } => *safety_classification,
385            })
386            .min()
387            .unwrap_or(SafetyClassification::Safe)
388    }
389
390    #[inline]
391    pub fn to_minimum_safety_classification(&self, safety: SafetyClassification) -> Self {
392        let min_safety = self.get_minimum_safety_classification();
393        if min_safety > safety {
394            return Self::new();
395        }
396
397        Self {
398            operations: self.operations.iter().filter(|op| op.get_safety_classification() <= safety).cloned().collect(),
399        }
400    }
401
402    /// Determines whether the plan is empty.
403    pub fn is_empty(&self) -> bool {
404        self.operations.is_empty()
405    }
406
407    /// Returns the number of operations in the plan.
408    pub fn len(&self) -> usize {
409        self.operations.len()
410    }
411
412    /// Executes the sequence of operations in the plan to a given text content.
413    ///
414    /// This function processes the original content according to the operations specified
415    /// in the plan. It only executes operations that have a safety classification equal to or less than
416    /// the provided maximum safety classification. The result is a `ChangeSet` object containing the
417    /// modified content and the list of changes made.
418    ///
419    /// # Arguments
420    ///
421    /// * `content` - The original text to which the operations will be applied.
422    /// * `max_safety_classification` - The maximum allowable safety classification for operations to be applied.
423    ///
424    /// # Returns
425    ///
426    /// A `ChangeSet` object representing the changes made to the content.
427    #[inline]
428    pub fn execute(&self, content: &str) -> ChangeSet {
429        let mut operations = self.operations.clone();
430
431        fix_overlapping_operations(&mut operations);
432
433        let content_len = content.len() as u32;
434
435        // Adjust out-of-bounds operations
436        operations = operations
437            .into_iter()
438            .filter_map(|op| match op {
439                FixOperation::Insert { offset, text, safety_classification } => {
440                    let adjusted_offset = offset.min(content_len);
441
442                    Some(FixOperation::Insert { offset: adjusted_offset, text, safety_classification })
443                }
444                FixOperation::Replace { range, text, safety_classification } => {
445                    if range.start == range.end {
446                        // Empty range, treat as insert
447                        let adjusted_offset = range.start.min(content_len);
448
449                        Some(FixOperation::Insert { offset: adjusted_offset, text, safety_classification })
450                    } else if range.start >= content_len || range.start > range.end {
451                        tracing::trace!("skipping invalid replace operation at range {:?} `{}`", range, text,);
452
453                        // Ignore out-of-bounds or invalid ranges
454                        None
455                    } else {
456                        let adjusted_end = range.end.min(content_len);
457
458                        Some(FixOperation::Replace { range: range.start..adjusted_end, text, safety_classification })
459                    }
460                }
461                FixOperation::Delete { range, safety_classification } => {
462                    if range.start >= content_len || range.start >= range.end {
463                        tracing::trace!("skipping invalid delete operation at range {:?}", range);
464
465                        // Ignore out-of-bounds or invalid ranges
466                        None
467                    } else {
468                        let adjusted_end = range.end.min(content_len);
469
470                        Some(FixOperation::Delete { range: range.start..adjusted_end, safety_classification })
471                    }
472                }
473            })
474            .collect::<Vec<_>>();
475
476        // Sort operations by start position
477        operations.sort_by_key(|op| match op {
478            FixOperation::Insert { offset, .. } => *offset,
479            FixOperation::Replace { range, .. } => range.start,
480            FixOperation::Delete { range, .. } => range.start,
481        });
482
483        let mut changes = Vec::new();
484        let mut current_position = 0;
485        let mut op_iter = operations.into_iter().peekable();
486
487        while current_position < content_len || op_iter.peek().is_some() {
488            if let Some(op) = op_iter.peek() {
489                match op {
490                    FixOperation::Insert { offset, text, .. } => {
491                        if *offset <= current_position {
492                            // Insert at the current position
493                            changes.push(Change::Inserted(text.clone()));
494                            op_iter.next();
495                        } else {
496                            // Consume unchanged content up to the insert position
497                            let end = offset.min(&content_len);
498                            if current_position < *end {
499                                changes.push(Change::Unchanged(
500                                    content[current_position as usize..*end as usize].to_string(),
501                                ));
502                                current_position = *end;
503                            }
504                        }
505                    }
506                    FixOperation::Replace { range, text, .. } => {
507                        if range.start <= current_position {
508                            // Replace at the current position
509                            let delete_len = range.end - current_position;
510                            if delete_len > 0 {
511                                changes.push(Change::Deleted(
512                                    content[current_position as usize..range.end as usize].to_string(),
513                                ));
514                            }
515                            changes.push(Change::Inserted(text.clone()));
516                            current_position = range.end;
517                            op_iter.next();
518                        } else {
519                            // Consume unchanged content up to the replace position
520                            let end = range.start.min(content_len);
521                            if current_position < end {
522                                changes.push(Change::Unchanged(
523                                    content[current_position as usize..end as usize].to_string(),
524                                ));
525                                current_position = end;
526                            }
527                        }
528                    }
529                    FixOperation::Delete { range, .. } => {
530                        if range.start <= current_position {
531                            // Delete at the current position
532                            let delete_len = range.end - current_position;
533                            if delete_len > 0 {
534                                changes.push(Change::Deleted(
535                                    content[current_position as usize..range.end as usize].to_string(),
536                                ));
537                            }
538                            current_position = range.end;
539                            op_iter.next();
540                        } else {
541                            // Consume unchanged content up to the delete position
542                            let end = range.start.min(content_len);
543                            if current_position < end {
544                                changes.push(Change::Unchanged(
545                                    content[current_position as usize..end as usize].to_string(),
546                                ));
547                                current_position = end;
548                            }
549                        }
550                    }
551                }
552            } else {
553                // No more operations, consume remaining content
554                if current_position < content_len {
555                    changes.push(Change::Unchanged(content[current_position as usize..].to_string()));
556                    current_position = content_len;
557                }
558            }
559        }
560
561        ChangeSet { changes }
562    }
563}
564
565impl IntoIterator for FixPlan {
566    type Item = FixOperation;
567    type IntoIter = std::vec::IntoIter<FixOperation>;
568
569    fn into_iter(self) -> Self::IntoIter {
570        self.operations.into_iter()
571    }
572}
573
574impl IntoIterator for ChangeSet {
575    type Item = Change;
576    type IntoIter = std::vec::IntoIter<Self::Item>;
577
578    fn into_iter(self) -> Self::IntoIter {
579        self.changes.into_iter()
580    }
581}
582
583impl FromIterator<Change> for ChangeSet {
584    fn from_iter<T: IntoIterator<Item = Change>>(iter: T) -> Self {
585        let changes = iter.into_iter().collect();
586        ChangeSet { changes }
587    }
588}
589
590impl FromIterator<FixOperation> for FixPlan {
591    fn from_iter<T: IntoIterator<Item = FixOperation>>(iter: T) -> Self {
592        let operations = iter.into_iter().collect();
593        FixPlan { operations }
594    }
595}
596
597impl FromIterator<FixPlan> for FixPlan {
598    fn from_iter<T: IntoIterator<Item = FixPlan>>(iter: T) -> Self {
599        let operations = iter.into_iter().flat_map(|plan| plan.operations).collect();
600
601        FixPlan { operations }
602    }
603}
604fn fix_overlapping_operations(operations: &mut Vec<FixOperation>) {
605    let mut filtered_operations = Vec::new();
606
607    for op in operations.iter() {
608        match op {
609            FixOperation::Delete { range, .. } => {
610                let mut should_add = true;
611                filtered_operations.retain(|existing_op| {
612                    match existing_op {
613                        FixOperation::Delete { range: existing_range, .. } => {
614                            if existing_range.contains(&range.start) && existing_range.contains(&(range.end - 1)) {
615                                // `op` is entirely within `existing_op`'s range
616                                should_add = false;
617                                return true;
618                            } else if range.contains(&existing_range.start) && range.contains(&(existing_range.end - 1))
619                            {
620                                // `existing_op` is entirely within `op`'s range, so remove it
621                                return false;
622                            }
623                            true
624                        }
625                        FixOperation::Replace { range: replace_range, .. } => {
626                            if range.start <= replace_range.start && range.end >= replace_range.end {
627                                // `Delete` operation completely covers `Replace` range, remove `Replace`
628                                return false;
629                            }
630                            if range.start <= replace_range.end && range.end > replace_range.start {
631                                // `Replace` falls within a `Delete`, ignore `Replace`
632                                return false;
633                            }
634                            true
635                        }
636                        _ => true,
637                    }
638                });
639
640                if should_add {
641                    filtered_operations.push(op.clone());
642                }
643            }
644            FixOperation::Replace { range, .. } => {
645                let mut should_add = true;
646                for existing_op in &filtered_operations {
647                    if let FixOperation::Delete { range: delete_range, .. } = existing_op
648                        && delete_range.start <= range.start
649                        && delete_range.end >= range.end
650                    {
651                        // `Replace` falls within a `Delete`, so ignore `Replace`
652                        should_add = false;
653                        break;
654                    }
655                }
656                if should_add {
657                    filtered_operations.push(op.clone());
658                }
659            }
660            _ => filtered_operations.push(op.clone()),
661        }
662    }
663
664    // Replace original operations with filtered ones
665    *operations = filtered_operations;
666}
667
668#[cfg(test)]
669mod tests {
670    use super::*;
671
672    use pretty_assertions::assert_eq;
673
674    #[test]
675    fn test_operations() {
676        let content = "$a = ($b) + ($c);";
677
678        let expected_safe = "$a = $b * $c;";
679        let expected_potentially_unsafe = "$a = ($b * $c);";
680        let expected_unsafe = "$a = ((int) $b * (int) $c);";
681
682        let mut fix = FixPlan::new();
683
684        fix.delete(5..6, SafetyClassification::Safe); // remove the `(` before $b
685        fix.delete(8..9, SafetyClassification::Safe); // remove the `)` after $b
686        fix.insert(6, "(int) ", SafetyClassification::Unsafe); // insert `(int) ` before $b
687        fix.replace(10..11, "*", SafetyClassification::Safe); // replace `+` with `*`
688        fix.delete(12..13, SafetyClassification::Safe); // remove the `(` before $c
689        fix.insert(13, "(int) ", SafetyClassification::Unsafe); // insert `(int) ` before $c
690        fix.delete(15..16, SafetyClassification::Safe); // remove the `)` after $c
691        fix.insert(5, "(", SafetyClassification::PotentiallyUnsafe); // insert the outer `(` before $b
692        fix.insert(16, ")", SafetyClassification::PotentiallyUnsafe); // insert the outer `)` after $c
693
694        let safe_result = fix.to_minimum_safety_classification(SafetyClassification::Safe).execute(content);
695        let potentially_unsafe_result =
696            fix.to_minimum_safety_classification(SafetyClassification::PotentiallyUnsafe).execute(content);
697        let unsafe_result = fix.to_minimum_safety_classification(SafetyClassification::Unsafe).execute(content);
698
699        assert_eq!(safe_result.get_fixed(), expected_safe);
700        assert_eq!(potentially_unsafe_result.get_fixed(), expected_potentially_unsafe);
701        assert_eq!(unsafe_result.get_fixed(), expected_unsafe);
702
703        assert_eq!(
704            safe_result.changes,
705            vec![
706                Change::Unchanged("$a = ".to_string()),
707                Change::Deleted("(".to_string()),
708                Change::Unchanged("$b".to_string()),
709                Change::Deleted(")".to_string()),
710                Change::Unchanged(" ".to_string()),
711                Change::Deleted("+".to_string()),
712                Change::Inserted("*".to_string()),
713                Change::Unchanged(" ".to_string()),
714                Change::Deleted("(".to_string()),
715                Change::Unchanged("$c".to_string()),
716                Change::Deleted(")".to_string()),
717                Change::Unchanged(";".to_string()),
718            ]
719        );
720    }
721
722    #[test]
723    fn test_insert_within_bounds() {
724        // Insert at a valid position within the content
725        let content = "Hello World";
726        let mut fix = FixPlan::new();
727        fix.insert(6, "Beautiful ", SafetyClassification::Safe);
728        let result = fix.execute(content);
729        assert_eq!(result.get_fixed(), "Hello Beautiful World");
730    }
731
732    #[test]
733    fn test_insert_at_end() {
734        // Insert at an offset equal to content length
735        let content = "Hello";
736        let mut fix = FixPlan::new();
737        fix.insert(5, " World", SafetyClassification::Safe);
738        let result = fix.execute(content);
739        assert_eq!(result.get_fixed(), "Hello World");
740    }
741
742    #[test]
743    fn test_insert_beyond_bounds() {
744        // Insert at an offset beyond content length
745        let content = "Hello";
746        let mut fix = FixPlan::new();
747        fix.insert(100, " World", SafetyClassification::Safe);
748        let result = fix.execute(content);
749        assert_eq!(result.get_fixed(), "Hello World"); // Inserted at the end
750    }
751
752    #[test]
753    fn test_delete_within_bounds() {
754        // Delete a valid range within the content
755        let content = "Hello Beautiful World";
756        let mut fix = FixPlan::new();
757        fix.delete(6..16, SafetyClassification::Safe);
758        let result = fix.execute(content);
759        assert_eq!(result.get_fixed(), "Hello World");
760    }
761
762    #[test]
763    fn test_delete_beyond_bounds() {
764        // Delete a range that is partially out of bounds
765        let content = "Hello World";
766        let mut fix = FixPlan::new();
767        fix.delete(6..100, SafetyClassification::Safe);
768        let result = fix.execute(content);
769        assert_eq!(result.get_fixed(), "Hello "); // Deleted from offset 6 to end
770    }
771
772    #[test]
773    fn test_delete_out_of_bounds() {
774        // Delete a range completely out of bounds
775        let content = "Hello";
776        let mut fix = FixPlan::new();
777        fix.delete(10..20, SafetyClassification::Safe);
778        let result = fix.execute(content);
779        assert_eq!(result.get_fixed(), "Hello"); // No changes
780    }
781
782    #[test]
783    fn test_replace_within_bounds() {
784        // Replace a valid range within the content
785        let content = "Hello World";
786        let mut fix = FixPlan::new();
787        fix.replace(6..11, "Rust", SafetyClassification::Safe);
788        let result = fix.execute(content);
789        assert_eq!(result.get_fixed(), "Hello Rust");
790    }
791
792    #[test]
793    fn test_replace_beyond_bounds() {
794        // Replace a range that is partially out of bounds
795        let content = "Hello World";
796        let mut fix = FixPlan::new();
797        fix.replace(6..100, "Rustaceans", SafetyClassification::Safe);
798        let result = fix.execute(content);
799        assert_eq!(result.get_fixed(), "Hello Rustaceans"); // Replaced from offset 6 to end
800    }
801
802    #[test]
803    fn test_overlapping_deletes() {
804        let content = "Hello World";
805        let mut fix = FixPlan::new();
806        fix.delete(3..9, SafetyClassification::Safe);
807        fix.delete(4..8, SafetyClassification::Safe);
808        fix.delete(5..7, SafetyClassification::Safe);
809        fix.replace(5..7, "xx", SafetyClassification::Safe);
810        fix.delete(10..11, SafetyClassification::Safe);
811        let result = fix.execute(content);
812        assert_eq!(result.get_fixed(), "Hell");
813    }
814
815    #[test]
816    fn test_replace_out_of_bounds() {
817        // Replace a range completely out of bounds
818        let content = "Hello";
819        let mut fix = FixPlan::new();
820        fix.replace(10..20, "Hi", SafetyClassification::Safe);
821
822        let result = fix.execute(content);
823        assert_eq!(result.get_fixed(), "Hello"); // No changes
824    }
825
826    #[test]
827    fn test_overlapping_operations() {
828        // Overlapping delete and replace operations
829        let content = "The quick brown fox jumps over the lazy dog.";
830        let mut fix = FixPlan::new();
831        fix.delete(10..19, SafetyClassification::Safe); // Delete "brown fox"
832        fix.insert(16, "cat", SafetyClassification::Safe); // Replace "fox" (which is partially deleted)
833        let result = fix.execute(content);
834        assert_eq!(result.get_fixed(), "The quick cat jumps over the lazy dog.");
835        // "brown fox" deleted, "cat" inserted
836    }
837
838    #[test]
839    fn test_insert_at_zero() {
840        // Insert at the beginning of the content
841        let content = "World";
842        let mut fix = FixPlan::new();
843        fix.insert(0, "Hello ", SafetyClassification::Safe);
844        let result = fix.execute(content);
845        assert_eq!(result.get_fixed(), "Hello World");
846    }
847
848    #[test]
849    fn test_empty_content_insert() {
850        // Insert into empty content
851        let content = "";
852        let mut fix = FixPlan::new();
853        fix.insert(0, "Hello World", SafetyClassification::Safe);
854
855        let result = fix.execute(content);
856        assert_eq!(result.get_fixed(), "Hello World");
857    }
858
859    #[test]
860    fn test_empty_content_delete() {
861        // Attempt to delete from empty content
862        let content = "";
863        let mut fix = FixPlan::new();
864        fix.delete(0..10, SafetyClassification::Safe);
865
866        let result = fix.execute(content);
867        assert_eq!(result.get_fixed(), ""); // No changes
868    }
869
870    #[test]
871    fn test_multiple_operations_ordering() {
872        // Multiple operations affecting ordering
873        let content = "abcdef";
874        let mut fix = FixPlan::new();
875        fix.delete(2..4, SafetyClassification::Safe); // Delete "cd"
876        fix.insert(2, "XY", SafetyClassification::Safe); // Insert "XY" at position 2
877        fix.replace(0..2, "12", SafetyClassification::Safe); // Replace "ab" with "12"
878        fix.insert(6, "34", SafetyClassification::Safe); // Insert "34" at the end (after fix)
879
880        let result = fix.execute(content);
881        assert_eq!(result.get_fixed(), "12XYef34");
882    }
883
884    #[test]
885    #[allow(clippy::reversed_empty_ranges)]
886    fn test_operations_with_invalid_ranges() {
887        // Operations with invalid ranges (start >= end)
888        let content = "Hello World";
889        let mut fix = FixPlan::new();
890
891        fix.delete(5..3, SafetyClassification::Safe); // Invalid range
892        fix.replace(8..8, "Test", SafetyClassification::Safe); // Empty range, treated as insert
893        fix.insert(6, "Beautiful ", SafetyClassification::Safe); // Valid insert
894
895        let result = fix.execute(content);
896        assert_eq!(result.get_fixed(), "Hello Beautiful WoTestrld"); // Only the insert is applied
897    }
898
899    #[test]
900    fn test_happy_path() {
901        let content = "<?php for (;true;): endfor;";
902        let mut fix = FixPlan::new();
903
904        fix.replace(6..12, "while(", SafetyClassification::Safe);
905        fix.delete(16..17, SafetyClassification::Safe);
906        fix.replace(20..26, "endwhile", SafetyClassification::Safe);
907
908        let result = fix.execute(content);
909        assert_eq!(result.get_fixed(), "<?php while(true): endwhile;");
910    }
911
912    #[test]
913    fn test_happy_path_2() {
914        let content = "<?php for (;;): endfor;";
915        let mut fix = FixPlan::new();
916
917        fix.replace(6..10, "while", SafetyClassification::Safe);
918        fix.delete(11..12, SafetyClassification::Safe);
919        fix.insert(12, "true", SafetyClassification::Safe);
920        fix.delete(12..13, SafetyClassification::Safe);
921        fix.replace(16..22, "endwhile", SafetyClassification::Safe);
922
923        let result = fix.execute(content);
924        assert_eq!(result.get_fixed(), "<?php while(true): endwhile;");
925    }
926
927    #[test]
928    fn test_happy_path_3() {
929        let content = "<?php for(;;): endfor;";
930        let mut fix = FixPlan::new();
931
932        fix.delete(6..9, SafetyClassification::Safe);
933        fix.insert(6, "while", SafetyClassification::Safe);
934        fix.delete(10..11, SafetyClassification::Safe);
935        fix.insert(11, "true", SafetyClassification::Safe);
936        fix.delete(11..12, SafetyClassification::Safe);
937        fix.replace(15..21, "endwhile", SafetyClassification::Safe);
938
939        let result = fix.execute(content);
940        assert_eq!(result.get_fixed(), "<?php while(true): endwhile;");
941    }
942}