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: usize,
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<usize>,
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<usize>,
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: usize, 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<usize>, 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<usize>, 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();
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(content[current_position..*end].to_string()));
500                                current_position = *end;
501                            }
502                        }
503                    }
504                    FixOperation::Replace { range, text, .. } => {
505                        if range.start <= current_position {
506                            // Replace at the current position
507                            let delete_len = range.end - current_position;
508                            if delete_len > 0 {
509                                changes.push(Change::Deleted(content[current_position..range.end].to_string()));
510                            }
511                            changes.push(Change::Inserted(text.clone()));
512                            current_position = range.end;
513                            op_iter.next();
514                        } else {
515                            // Consume unchanged content up to the replace position
516                            let end = range.start.min(content_len);
517                            if current_position < end {
518                                changes.push(Change::Unchanged(content[current_position..end].to_string()));
519                                current_position = end;
520                            }
521                        }
522                    }
523                    FixOperation::Delete { range, .. } => {
524                        if range.start <= current_position {
525                            // Delete at the current position
526                            let delete_len = range.end - current_position;
527                            if delete_len > 0 {
528                                changes.push(Change::Deleted(content[current_position..range.end].to_string()));
529                            }
530                            current_position = range.end;
531                            op_iter.next();
532                        } else {
533                            // Consume unchanged content up to the delete position
534                            let end = range.start.min(content_len);
535                            if current_position < end {
536                                changes.push(Change::Unchanged(content[current_position..end].to_string()));
537                                current_position = end;
538                            }
539                        }
540                    }
541                }
542            } else {
543                // No more operations, consume remaining content
544                if current_position < content_len {
545                    changes.push(Change::Unchanged(content[current_position..].to_string()));
546                    current_position = content_len;
547                }
548            }
549        }
550
551        ChangeSet { changes }
552    }
553}
554
555impl IntoIterator for FixPlan {
556    type Item = FixOperation;
557    type IntoIter = std::vec::IntoIter<FixOperation>;
558
559    fn into_iter(self) -> Self::IntoIter {
560        self.operations.into_iter()
561    }
562}
563
564impl IntoIterator for ChangeSet {
565    type Item = Change;
566    type IntoIter = std::vec::IntoIter<Self::Item>;
567
568    fn into_iter(self) -> Self::IntoIter {
569        self.changes.into_iter()
570    }
571}
572
573impl FromIterator<Change> for ChangeSet {
574    fn from_iter<T: IntoIterator<Item = Change>>(iter: T) -> Self {
575        let changes = iter.into_iter().collect();
576        ChangeSet { changes }
577    }
578}
579
580impl FromIterator<FixOperation> for FixPlan {
581    fn from_iter<T: IntoIterator<Item = FixOperation>>(iter: T) -> Self {
582        let operations = iter.into_iter().collect();
583        FixPlan { operations }
584    }
585}
586
587impl FromIterator<FixPlan> for FixPlan {
588    fn from_iter<T: IntoIterator<Item = FixPlan>>(iter: T) -> Self {
589        let operations = iter.into_iter().flat_map(|plan| plan.operations).collect();
590
591        FixPlan { operations }
592    }
593}
594fn fix_overlapping_operations(operations: &mut Vec<FixOperation>) {
595    let mut filtered_operations = Vec::new();
596
597    for op in operations.iter() {
598        match op {
599            FixOperation::Delete { range, .. } => {
600                let mut should_add = true;
601                filtered_operations.retain(|existing_op| {
602                    match existing_op {
603                        FixOperation::Delete { range: existing_range, .. } => {
604                            if existing_range.contains(&range.start) && existing_range.contains(&(range.end - 1)) {
605                                // `op` is entirely within `existing_op`'s range
606                                should_add = false;
607                                return true;
608                            } else if range.contains(&existing_range.start) && range.contains(&(existing_range.end - 1))
609                            {
610                                // `existing_op` is entirely within `op`'s range, so remove it
611                                return false;
612                            }
613                            true
614                        }
615                        FixOperation::Replace { range: replace_range, .. } => {
616                            if range.start <= replace_range.start && range.end >= replace_range.end {
617                                // `Delete` operation completely covers `Replace` range, remove `Replace`
618                                return false;
619                            }
620                            if range.start <= replace_range.end && range.end > replace_range.start {
621                                // `Replace` falls within a `Delete`, ignore `Replace`
622                                return false;
623                            }
624                            true
625                        }
626                        _ => true,
627                    }
628                });
629
630                if should_add {
631                    filtered_operations.push(op.clone());
632                }
633            }
634            FixOperation::Replace { range, .. } => {
635                let mut should_add = true;
636                for existing_op in &filtered_operations {
637                    if let FixOperation::Delete { range: delete_range, .. } = existing_op
638                        && delete_range.start <= range.start
639                        && delete_range.end >= range.end
640                    {
641                        // `Replace` falls within a `Delete`, so ignore `Replace`
642                        should_add = false;
643                        break;
644                    }
645                }
646                if should_add {
647                    filtered_operations.push(op.clone());
648                }
649            }
650            _ => filtered_operations.push(op.clone()),
651        }
652    }
653
654    // Replace original operations with filtered ones
655    *operations = filtered_operations;
656}
657
658#[cfg(test)]
659mod tests {
660    use super::*;
661
662    use pretty_assertions::assert_eq;
663
664    #[test]
665    fn test_operations() {
666        let content = "$a = ($b) + ($c);";
667
668        let expected_safe = "$a = $b * $c;";
669        let expected_potentially_unsafe = "$a = ($b * $c);";
670        let expected_unsafe = "$a = ((int) $b * (int) $c);";
671
672        let mut fix = FixPlan::new();
673
674        fix.delete(5..6, SafetyClassification::Safe); // remove the `(` before $b
675        fix.delete(8..9, SafetyClassification::Safe); // remove the `)` after $b
676        fix.insert(6, "(int) ", SafetyClassification::Unsafe); // insert `(int) ` before $b
677        fix.replace(10..11, "*", SafetyClassification::Safe); // replace `+` with `*`
678        fix.delete(12..13, SafetyClassification::Safe); // remove the `(` before $c
679        fix.insert(13, "(int) ", SafetyClassification::Unsafe); // insert `(int) ` before $c
680        fix.delete(15..16, SafetyClassification::Safe); // remove the `)` after $c
681        fix.insert(5, "(", SafetyClassification::PotentiallyUnsafe); // insert the outer `(` before $b
682        fix.insert(16, ")", SafetyClassification::PotentiallyUnsafe); // insert the outer `)` after $c
683
684        let safe_result = fix.to_minimum_safety_classification(SafetyClassification::Safe).execute(content);
685        let potentially_unsafe_result =
686            fix.to_minimum_safety_classification(SafetyClassification::PotentiallyUnsafe).execute(content);
687        let unsafe_result = fix.to_minimum_safety_classification(SafetyClassification::Unsafe).execute(content);
688
689        assert_eq!(safe_result.get_fixed(), expected_safe);
690        assert_eq!(potentially_unsafe_result.get_fixed(), expected_potentially_unsafe);
691        assert_eq!(unsafe_result.get_fixed(), expected_unsafe);
692
693        assert_eq!(
694            safe_result.changes,
695            vec![
696                Change::Unchanged("$a = ".to_string()),
697                Change::Deleted("(".to_string()),
698                Change::Unchanged("$b".to_string()),
699                Change::Deleted(")".to_string()),
700                Change::Unchanged(" ".to_string()),
701                Change::Deleted("+".to_string()),
702                Change::Inserted("*".to_string()),
703                Change::Unchanged(" ".to_string()),
704                Change::Deleted("(".to_string()),
705                Change::Unchanged("$c".to_string()),
706                Change::Deleted(")".to_string()),
707                Change::Unchanged(";".to_string()),
708            ]
709        );
710    }
711
712    #[test]
713    fn test_insert_within_bounds() {
714        // Insert at a valid position within the content
715        let content = "Hello World";
716        let mut fix = FixPlan::new();
717        fix.insert(6, "Beautiful ", SafetyClassification::Safe);
718        let result = fix.execute(content);
719        assert_eq!(result.get_fixed(), "Hello Beautiful World");
720    }
721
722    #[test]
723    fn test_insert_at_end() {
724        // Insert at an offset equal to content length
725        let content = "Hello";
726        let mut fix = FixPlan::new();
727        fix.insert(5, " World", SafetyClassification::Safe);
728        let result = fix.execute(content);
729        assert_eq!(result.get_fixed(), "Hello World");
730    }
731
732    #[test]
733    fn test_insert_beyond_bounds() {
734        // Insert at an offset beyond content length
735        let content = "Hello";
736        let mut fix = FixPlan::new();
737        fix.insert(100, " World", SafetyClassification::Safe);
738        let result = fix.execute(content);
739        assert_eq!(result.get_fixed(), "Hello World"); // Inserted at the end
740    }
741
742    #[test]
743    fn test_delete_within_bounds() {
744        // Delete a valid range within the content
745        let content = "Hello Beautiful World";
746        let mut fix = FixPlan::new();
747        fix.delete(6..16, SafetyClassification::Safe);
748        let result = fix.execute(content);
749        assert_eq!(result.get_fixed(), "Hello World");
750    }
751
752    #[test]
753    fn test_delete_beyond_bounds() {
754        // Delete a range that is partially out of bounds
755        let content = "Hello World";
756        let mut fix = FixPlan::new();
757        fix.delete(6..100, SafetyClassification::Safe);
758        let result = fix.execute(content);
759        assert_eq!(result.get_fixed(), "Hello "); // Deleted from offset 6 to end
760    }
761
762    #[test]
763    fn test_delete_out_of_bounds() {
764        // Delete a range completely out of bounds
765        let content = "Hello";
766        let mut fix = FixPlan::new();
767        fix.delete(10..20, SafetyClassification::Safe);
768        let result = fix.execute(content);
769        assert_eq!(result.get_fixed(), "Hello"); // No changes
770    }
771
772    #[test]
773    fn test_replace_within_bounds() {
774        // Replace a valid range within the content
775        let content = "Hello World";
776        let mut fix = FixPlan::new();
777        fix.replace(6..11, "Rust", SafetyClassification::Safe);
778        let result = fix.execute(content);
779        assert_eq!(result.get_fixed(), "Hello Rust");
780    }
781
782    #[test]
783    fn test_replace_beyond_bounds() {
784        // Replace a range that is partially out of bounds
785        let content = "Hello World";
786        let mut fix = FixPlan::new();
787        fix.replace(6..100, "Rustaceans", SafetyClassification::Safe);
788        let result = fix.execute(content);
789        assert_eq!(result.get_fixed(), "Hello Rustaceans"); // Replaced from offset 6 to end
790    }
791
792    #[test]
793    fn test_overlapping_deletes() {
794        let content = "Hello World";
795        let mut fix = FixPlan::new();
796        fix.delete(3..9, SafetyClassification::Safe);
797        fix.delete(4..8, SafetyClassification::Safe);
798        fix.delete(5..7, SafetyClassification::Safe);
799        fix.replace(5..7, "xx", SafetyClassification::Safe);
800        fix.delete(10..11, SafetyClassification::Safe);
801        let result = fix.execute(content);
802        assert_eq!(result.get_fixed(), "Hell");
803    }
804
805    #[test]
806    fn test_replace_out_of_bounds() {
807        // Replace a range completely out of bounds
808        let content = "Hello";
809        let mut fix = FixPlan::new();
810        fix.replace(10..20, "Hi", SafetyClassification::Safe);
811
812        let result = fix.execute(content);
813        assert_eq!(result.get_fixed(), "Hello"); // No changes
814    }
815
816    #[test]
817    fn test_overlapping_operations() {
818        // Overlapping delete and replace operations
819        let content = "The quick brown fox jumps over the lazy dog.";
820        let mut fix = FixPlan::new();
821        fix.delete(10..19, SafetyClassification::Safe); // Delete "brown fox"
822        fix.insert(16, "cat", SafetyClassification::Safe); // Replace "fox" (which is partially deleted)
823        let result = fix.execute(content);
824        assert_eq!(result.get_fixed(), "The quick cat jumps over the lazy dog.");
825        // "brown fox" deleted, "cat" inserted
826    }
827
828    #[test]
829    fn test_insert_at_zero() {
830        // Insert at the beginning of the content
831        let content = "World";
832        let mut fix = FixPlan::new();
833        fix.insert(0, "Hello ", SafetyClassification::Safe);
834        let result = fix.execute(content);
835        assert_eq!(result.get_fixed(), "Hello World");
836    }
837
838    #[test]
839    fn test_empty_content_insert() {
840        // Insert into empty content
841        let content = "";
842        let mut fix = FixPlan::new();
843        fix.insert(0, "Hello World", SafetyClassification::Safe);
844
845        let result = fix.execute(content);
846        assert_eq!(result.get_fixed(), "Hello World");
847    }
848
849    #[test]
850    fn test_empty_content_delete() {
851        // Attempt to delete from empty content
852        let content = "";
853        let mut fix = FixPlan::new();
854        fix.delete(0..10, SafetyClassification::Safe);
855
856        let result = fix.execute(content);
857        assert_eq!(result.get_fixed(), ""); // No changes
858    }
859
860    #[test]
861    fn test_multiple_operations_ordering() {
862        // Multiple operations affecting ordering
863        let content = "abcdef";
864        let mut fix = FixPlan::new();
865        fix.delete(2..4, SafetyClassification::Safe); // Delete "cd"
866        fix.insert(2, "XY", SafetyClassification::Safe); // Insert "XY" at position 2
867        fix.replace(0..2, "12", SafetyClassification::Safe); // Replace "ab" with "12"
868        fix.insert(6, "34", SafetyClassification::Safe); // Insert "34" at the end (after fix)
869
870        let result = fix.execute(content);
871        assert_eq!(result.get_fixed(), "12XYef34");
872    }
873
874    #[test]
875    #[allow(clippy::reversed_empty_ranges)]
876    fn test_operations_with_invalid_ranges() {
877        // Operations with invalid ranges (start >= end)
878        let content = "Hello World";
879        let mut fix = FixPlan::new();
880
881        fix.delete(5..3, SafetyClassification::Safe); // Invalid range
882        fix.replace(8..8, "Test", SafetyClassification::Safe); // Empty range, treated as insert
883        fix.insert(6, "Beautiful ", SafetyClassification::Safe); // Valid insert
884
885        let result = fix.execute(content);
886        assert_eq!(result.get_fixed(), "Hello Beautiful WoTestrld"); // Only the insert is applied
887    }
888
889    #[test]
890    fn test_happy_path() {
891        let content = "<?php for (;true;): endfor;";
892        let mut fix = FixPlan::new();
893
894        fix.replace(6..12, "while(", SafetyClassification::Safe);
895        fix.delete(16..17, SafetyClassification::Safe);
896        fix.replace(20..26, "endwhile", SafetyClassification::Safe);
897
898        let result = fix.execute(content);
899        assert_eq!(result.get_fixed(), "<?php while(true): endwhile;");
900    }
901
902    #[test]
903    fn test_happy_path_2() {
904        let content = "<?php for (;;): endfor;";
905        let mut fix = FixPlan::new();
906
907        fix.replace(6..10, "while", SafetyClassification::Safe);
908        fix.delete(11..12, SafetyClassification::Safe);
909        fix.insert(12, "true", SafetyClassification::Safe);
910        fix.delete(12..13, SafetyClassification::Safe);
911        fix.replace(16..22, "endwhile", SafetyClassification::Safe);
912
913        let result = fix.execute(content);
914        assert_eq!(result.get_fixed(), "<?php while(true): endwhile;");
915    }
916
917    #[test]
918    fn test_happy_path_3() {
919        let content = "<?php for(;;): endfor;";
920        let mut fix = FixPlan::new();
921
922        fix.delete(6..9, SafetyClassification::Safe);
923        fix.insert(6, "while", SafetyClassification::Safe);
924        fix.delete(10..11, SafetyClassification::Safe);
925        fix.insert(11, "true", SafetyClassification::Safe);
926        fix.delete(11..12, SafetyClassification::Safe);
927        fix.replace(15..21, "endwhile", SafetyClassification::Safe);
928
929        let result = fix.execute(content);
930        assert_eq!(result.get_fixed(), "<?php while(true): endwhile;");
931    }
932}