codemod_core/transform/
conflict.rs1use crate::pattern::matcher::Match;
9
10#[derive(Debug, Clone)]
16pub struct Conflict {
17 pub match_index: usize,
20 pub other_index: usize,
22 pub description: String,
24 pub suggestions: Vec<String>,
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum ConflictResolution {
31 Apply,
33 Skip,
35 ApplyAll,
37 SkipAll,
39}
40
41pub struct ConflictResolver;
47
48impl ConflictResolver {
49 pub fn detect_conflicts(matches: &[Match]) -> Vec<Conflict> {
55 let mut conflicts = Vec::new();
56
57 if matches.len() < 2 {
58 return conflicts;
59 }
60
61 let mut sorted: Vec<(usize, usize, usize)> = matches
63 .iter()
64 .enumerate()
65 .map(|(i, m)| (i, m.byte_range.start, m.byte_range.end))
66 .collect();
67 sorted.sort_by_key(|&(_, start, _)| start);
68
69 for window in sorted.windows(2) {
71 let (idx_a, _start_a, end_a) = window[0];
72 let (idx_b, start_b, _end_b) = window[1];
73
74 if start_b < end_a {
75 conflicts.push(Conflict {
76 match_index: idx_b,
77 other_index: idx_a,
78 description: format!(
79 "Match at byte offset {} overlaps with match at byte offset {} (overlap starts at byte {})",
80 matches[idx_b].byte_range.start,
81 matches[idx_a].byte_range.start,
82 start_b,
83 ),
84 suggestions: vec![
85 "Skip the later match".into(),
86 "Skip the earlier match".into(),
87 "Manually resolve the overlap".into(),
88 ],
89 });
90 }
91 }
92
93 conflicts
94 }
95
96 pub fn resolve_greedy(matches: &[Match]) -> Vec<usize> {
101 if matches.is_empty() {
102 return Vec::new();
103 }
104
105 let mut indexed: Vec<(usize, usize, usize)> = matches
106 .iter()
107 .enumerate()
108 .map(|(i, m)| (i, m.byte_range.start, m.byte_range.end))
109 .collect();
110 indexed.sort_by_key(|&(_, start, _)| start);
111
112 let mut kept = Vec::new();
113 let mut last_end: usize = 0;
114
115 for (idx, start, end) in indexed {
116 if start >= last_end {
117 kept.push(idx);
118 last_end = end;
119 }
120 }
121
122 kept
123 }
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129 use crate::pattern::matcher::{Match, Position};
130 use std::collections::HashMap;
131
132 fn make_match(start: usize, end: usize) -> Match {
133 Match {
134 byte_range: start..end,
135 start_position: Position {
136 line: 0,
137 column: start,
138 },
139 end_position: Position {
140 line: 0,
141 column: end,
142 },
143 matched_text: String::new(),
144 bindings: HashMap::new(),
145 }
146 }
147
148 #[test]
149 fn test_no_conflicts() {
150 let matches = vec![make_match(0, 5), make_match(10, 15)];
151 let conflicts = ConflictResolver::detect_conflicts(&matches);
152 assert!(conflicts.is_empty());
153 }
154
155 #[test]
156 fn test_overlapping_conflicts() {
157 let matches = vec![make_match(0, 10), make_match(5, 15)];
158 let conflicts = ConflictResolver::detect_conflicts(&matches);
159 assert_eq!(conflicts.len(), 1);
160 assert_eq!(conflicts[0].match_index, 1);
161 assert_eq!(conflicts[0].other_index, 0);
162 }
163
164 #[test]
165 fn test_adjacent_no_conflict() {
166 let matches = vec![make_match(0, 5), make_match(5, 10)];
167 let conflicts = ConflictResolver::detect_conflicts(&matches);
168 assert!(conflicts.is_empty());
169 }
170
171 #[test]
172 fn test_resolve_greedy() {
173 let matches = vec![make_match(0, 10), make_match(5, 15), make_match(15, 20)];
174 let kept = ConflictResolver::resolve_greedy(&matches);
175 assert_eq!(kept, vec![0, 2]);
176 }
177
178 #[test]
179 fn test_single_match() {
180 let matches = vec![make_match(0, 5)];
181 let conflicts = ConflictResolver::detect_conflicts(&matches);
182 assert!(conflicts.is_empty());
183 let kept = ConflictResolver::resolve_greedy(&matches);
184 assert_eq!(kept, vec![0]);
185 }
186}