1use core::ops::Range;
2
3use serde::Deserialize;
4use serde::Serialize;
5use strum::Display;
6
7#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize, PartialOrd, Ord)]
13#[serde(tag = "type", content = "value")]
14pub enum Change {
15 Unchanged(String),
17
18 Inserted(String),
20
21 Deleted(String),
23}
24
25#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
31pub struct ChangeSet {
32 changes: Vec<Change>,
37}
38
39#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Serialize, Deserialize, PartialOrd, Ord, Display)]
41#[serde(tag = "type", content = "value")]
42pub enum SafetyClassification {
43 Safe,
45 PotentiallyUnsafe,
47 Unsafe,
49}
50
51#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
58#[serde(tag = "type", content = "value")]
59pub enum FixOperation {
60 Insert {
62 offset: u32,
64 text: String,
66 safety_classification: SafetyClassification,
68 },
69
70 Replace {
72 range: Range<u32>,
74 text: String,
76 safety_classification: SafetyClassification,
78 },
79
80 Delete {
82 range: Range<u32>,
84 safety_classification: SafetyClassification,
86 },
87}
88
89#[derive(Default, Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
95pub struct FixPlan {
96 operations: Vec<FixOperation>,
98}
99
100impl ChangeSet {
101 pub fn new(changes: Vec<Change>) -> Self {
119 Self { changes }
120 }
121
122 pub fn from(changes: impl IntoIterator<Item = Change>) -> Self {
146 Self { changes: changes.into_iter().collect() }
147 }
148
149 #[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(_) => {} }
183 }
184
185 result
186 }
187
188 #[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(_) => {} Change::Unchanged(text) => result.push_str(text),
220 Change::Inserted(text) => result.push_str(text),
221 }
222 }
223 result
224 }
225
226 pub fn len(&self) -> usize {
228 self.changes.len()
229 }
230
231 pub fn is_empty(&self) -> bool {
233 self.changes.is_empty()
234 }
235
236 pub fn iter(&self) -> impl Iterator<Item = &Change> {
238 self.changes.iter()
239 }
240
241 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 pub fn new() -> Self {
266 Self { operations: Vec::default() }
267 }
268
269 pub fn from_operations(operations: Vec<FixOperation>) -> Self {
271 Self { operations }
272 }
273
274 pub fn operation(&mut self, operation: FixOperation) {
286 self.operations.push(operation);
287 }
288
289 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 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 pub fn delete(&mut self, range: Range<u32>, safety: SafetyClassification) {
339 self.operation(FixOperation::Delete { range, safety_classification: safety })
340 }
341
342 pub fn merge(&mut self, other: FixPlan) {
351 for op in other.operations {
352 self.operation(op);
353 }
354 }
355
356 pub fn get_operations(&self) -> &Vec<FixOperation> {
358 &self.operations
359 }
360
361 pub fn take_operations(self) -> Vec<FixOperation> {
365 self.operations
366 }
367
368 #[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 pub fn is_empty(&self) -> bool {
404 self.operations.is_empty()
405 }
406
407 pub fn len(&self) -> usize {
409 self.operations.len()
410 }
411
412 #[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 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 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 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 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 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 changes.push(Change::Inserted(text.clone()));
494 op_iter.next();
495 } else {
496 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 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 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 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 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 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 should_add = false;
617 return true;
618 } else if range.contains(&existing_range.start) && range.contains(&(existing_range.end - 1))
619 {
620 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 return false;
629 }
630 if range.start <= replace_range.end && range.end > replace_range.start {
631 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 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 *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); fix.delete(8..9, SafetyClassification::Safe); fix.insert(6, "(int) ", SafetyClassification::Unsafe); fix.replace(10..11, "*", SafetyClassification::Safe); fix.delete(12..13, SafetyClassification::Safe); fix.insert(13, "(int) ", SafetyClassification::Unsafe); fix.delete(15..16, SafetyClassification::Safe); fix.insert(5, "(", SafetyClassification::PotentiallyUnsafe); fix.insert(16, ")", SafetyClassification::PotentiallyUnsafe); 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 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 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 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"); }
751
752 #[test]
753 fn test_delete_within_bounds() {
754 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 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 "); }
771
772 #[test]
773 fn test_delete_out_of_bounds() {
774 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"); }
781
782 #[test]
783 fn test_replace_within_bounds() {
784 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 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"); }
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 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"); }
825
826 #[test]
827 fn test_overlapping_operations() {
828 let content = "The quick brown fox jumps over the lazy dog.";
830 let mut fix = FixPlan::new();
831 fix.delete(10..19, SafetyClassification::Safe); fix.insert(16, "cat", SafetyClassification::Safe); let result = fix.execute(content);
834 assert_eq!(result.get_fixed(), "The quick cat jumps over the lazy dog.");
835 }
837
838 #[test]
839 fn test_insert_at_zero() {
840 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 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 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(), ""); }
869
870 #[test]
871 fn test_multiple_operations_ordering() {
872 let content = "abcdef";
874 let mut fix = FixPlan::new();
875 fix.delete(2..4, SafetyClassification::Safe); fix.insert(2, "XY", SafetyClassification::Safe); fix.replace(0..2, "12", SafetyClassification::Safe); fix.insert(6, "34", SafetyClassification::Safe); 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 let content = "Hello World";
889 let mut fix = FixPlan::new();
890
891 fix.delete(5..3, SafetyClassification::Safe); fix.replace(8..8, "Test", SafetyClassification::Safe); fix.insert(6, "Beautiful ", SafetyClassification::Safe); let result = fix.execute(content);
896 assert_eq!(result.get_fixed(), "Hello Beautiful WoTestrld"); }
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}