1use crate::types::{Confidence, MergeScenario, ResolutionCandidate, ResolutionStrategy};
23
24pub trait PatternRule: Send + Sync {
26 fn name(&self) -> &str;
28
29 fn matches(&self, scenario: &MergeScenario<&str>) -> bool;
31
32 fn resolve(&self, scenario: &MergeScenario<&str>) -> String;
34
35 fn confidence(&self) -> Confidence;
37}
38
39pub struct PatternRegistry {
41 rules: Vec<Box<dyn PatternRule>>,
42}
43
44impl PatternRegistry {
45 pub fn new() -> Self {
47 Self {
48 rules: vec![
49 Box::new(WhitespaceOnlyRule),
50 Box::new(IdenticalChangeRule),
51 Box::new(BothAddLinesRule),
52 Box::new(OneEmptyRule),
53 Box::new(PrefixSuffixRule),
54 Box::new(ImportUnionRule),
55 Box::new(AdjacentEditRule),
56 ],
57 }
58 }
59
60 pub fn try_resolve(&self, scenario: &MergeScenario<&str>) -> Option<ResolutionCandidate> {
62 for rule in &self.rules {
63 if rule.matches(scenario) {
64 return Some(ResolutionCandidate {
65 content: rule.resolve(scenario),
66 confidence: rule.confidence(),
67 strategy: ResolutionStrategy::PatternRule,
68 });
69 }
70 }
71 None
72 }
73
74 pub fn try_resolve_all(&self, scenario: &MergeScenario<&str>) -> Vec<ResolutionCandidate> {
76 self.rules
77 .iter()
78 .filter(|rule| rule.matches(scenario))
79 .map(|rule| ResolutionCandidate {
80 content: rule.resolve(scenario),
81 confidence: rule.confidence(),
82 strategy: ResolutionStrategy::PatternRule,
83 })
84 .collect()
85 }
86}
87
88impl Default for PatternRegistry {
89 fn default() -> Self {
90 Self::new()
91 }
92}
93
94struct WhitespaceOnlyRule;
100
101impl PatternRule for WhitespaceOnlyRule {
102 fn name(&self) -> &str {
103 "whitespace-only"
104 }
105
106 fn matches(&self, scenario: &MergeScenario<&str>) -> bool {
107 let base_norm = normalize_whitespace(scenario.base);
108 let left_norm = normalize_whitespace(scenario.left);
109 let right_norm = normalize_whitespace(scenario.right);
110 base_norm == left_norm && base_norm == right_norm || left_norm == right_norm
112 }
113
114 fn resolve(&self, scenario: &MergeScenario<&str>) -> String {
115 scenario.left.to_string()
117 }
118
119 fn confidence(&self) -> Confidence {
120 Confidence::High
121 }
122}
123
124struct IdenticalChangeRule;
130
131impl PatternRule for IdenticalChangeRule {
132 fn name(&self) -> &str {
133 "identical-change"
134 }
135
136 fn matches(&self, scenario: &MergeScenario<&str>) -> bool {
137 scenario.left == scenario.right && scenario.left != scenario.base
138 }
139
140 fn resolve(&self, scenario: &MergeScenario<&str>) -> String {
141 scenario.left.to_string()
142 }
143
144 fn confidence(&self) -> Confidence {
145 Confidence::High
146 }
147}
148
149struct BothAddLinesRule;
156
157impl PatternRule for BothAddLinesRule {
158 fn name(&self) -> &str {
159 "both-add-lines"
160 }
161
162 fn matches(&self, scenario: &MergeScenario<&str>) -> bool {
163 let base = scenario.base.trim();
164 if !base.is_empty() {
165 return false;
166 }
167 !scenario.left.trim().is_empty() && !scenario.right.trim().is_empty()
169 }
170
171 fn resolve(&self, scenario: &MergeScenario<&str>) -> String {
172 let mut result = scenario.left.to_string();
173 if !result.ends_with('\n') {
174 result.push('\n');
175 }
176 result.push_str(scenario.right);
177 result
178 }
179
180 fn confidence(&self) -> Confidence {
181 Confidence::Medium
182 }
183}
184
185struct OneEmptyRule;
192
193impl PatternRule for OneEmptyRule {
194 fn name(&self) -> &str {
195 "one-empty"
196 }
197
198 fn matches(&self, scenario: &MergeScenario<&str>) -> bool {
199 let left_empty = scenario.left.trim().is_empty();
200 let right_empty = scenario.right.trim().is_empty();
201 (left_empty && !right_empty) || (!left_empty && right_empty)
202 }
203
204 fn resolve(&self, scenario: &MergeScenario<&str>) -> String {
205 if scenario.left.trim().is_empty() {
206 scenario.right.to_string()
207 } else {
208 scenario.left.to_string()
209 }
210 }
211
212 fn confidence(&self) -> Confidence {
213 Confidence::Medium
214 }
215}
216
217struct PrefixSuffixRule;
225
226impl PatternRule for PrefixSuffixRule {
227 fn name(&self) -> &str {
228 "prefix-suffix"
229 }
230
231 fn matches(&self, scenario: &MergeScenario<&str>) -> bool {
232 let left = scenario.left.trim();
233 let right = scenario.right.trim();
234 if left == right {
235 return false;
236 }
237 left.starts_with(right)
238 || right.starts_with(left)
239 || left.ends_with(right)
240 || right.ends_with(left)
241 }
242
243 fn resolve(&self, scenario: &MergeScenario<&str>) -> String {
244 let left = scenario.left.trim();
245 let right = scenario.right.trim();
246 if left.len() >= right.len() {
248 scenario.left.to_string()
249 } else {
250 scenario.right.to_string()
251 }
252 }
253
254 fn confidence(&self) -> Confidence {
255 Confidence::Medium
256 }
257}
258
259struct ImportUnionRule;
266
267impl PatternRule for ImportUnionRule {
268 fn name(&self) -> &str {
269 "import-union"
270 }
271
272 fn matches(&self, scenario: &MergeScenario<&str>) -> bool {
273 let all_imports = |text: &str| {
275 text.lines()
276 .filter(|l| !l.trim().is_empty())
277 .all(is_import_line)
278 };
279 all_imports(scenario.base) && all_imports(scenario.left) && all_imports(scenario.right)
280 }
281
282 fn resolve(&self, scenario: &MergeScenario<&str>) -> String {
283 let mut imports: Vec<String> = Vec::new();
284
285 for line in scenario.left.lines().chain(scenario.right.lines()) {
287 let trimmed = line.trim().to_string();
288 if !trimmed.is_empty() && !imports.contains(&trimmed) {
289 imports.push(trimmed);
290 }
291 }
292
293 imports.sort();
294 imports.join("\n")
295 }
296
297 fn confidence(&self) -> Confidence {
298 Confidence::High
299 }
300}
301
302struct AdjacentEditRule;
309
310impl PatternRule for AdjacentEditRule {
311 fn name(&self) -> &str {
312 "adjacent-edit"
313 }
314
315 fn matches(&self, scenario: &MergeScenario<&str>) -> bool {
316 let base_lines: Vec<&str> = scenario.base.lines().collect();
317 let left_lines: Vec<&str> = scenario.left.lines().collect();
318 let right_lines: Vec<&str> = scenario.right.lines().collect();
319
320 if base_lines.len() != left_lines.len() || base_lines.len() != right_lines.len() {
322 return false;
323 }
324
325 for i in 0..base_lines.len() {
327 let left_changed = base_lines[i] != left_lines[i];
328 let right_changed = base_lines[i] != right_lines[i];
329 if left_changed && right_changed {
330 return false;
331 }
332 }
333
334 let left_has_changes = base_lines
336 .iter()
337 .zip(left_lines.iter())
338 .any(|(b, l)| b != l);
339 let right_has_changes = base_lines
340 .iter()
341 .zip(right_lines.iter())
342 .any(|(b, r)| b != r);
343 left_has_changes && right_has_changes
344 }
345
346 fn resolve(&self, scenario: &MergeScenario<&str>) -> String {
347 let base_lines: Vec<&str> = scenario.base.lines().collect();
348 let left_lines: Vec<&str> = scenario.left.lines().collect();
349 let right_lines: Vec<&str> = scenario.right.lines().collect();
350
351 let mut result = Vec::new();
352 for i in 0..base_lines.len() {
353 if base_lines[i] != left_lines[i] {
354 result.push(left_lines[i]);
355 } else if base_lines[i] != right_lines[i] {
356 result.push(right_lines[i]);
357 } else {
358 result.push(base_lines[i]);
359 }
360 }
361
362 result.join("\n")
363 }
364
365 fn confidence(&self) -> Confidence {
366 Confidence::High
367 }
368}
369
370fn normalize_whitespace(s: &str) -> String {
375 s.split_whitespace().collect::<Vec<_>>().join(" ")
376}
377
378fn is_import_line(line: &str) -> bool {
379 let trimmed = line.trim();
380 trimmed.starts_with("import ")
381 || trimmed.starts_with("from ")
382 || trimmed.starts_with("use ")
383 || trimmed.starts_with("#include")
384 || trimmed.starts_with("require(")
385 || trimmed.starts_with("const ")
386 && (trimmed.contains("require(") || trimmed.contains("import("))
387}
388
389#[cfg(test)]
390mod tests {
391 use super::*;
392
393 #[test]
394 fn test_whitespace_only() {
395 let scenario = MergeScenario::new(
396 "int x = 1;",
397 "int x = 1;", "int x = 1;", );
400 let registry = PatternRegistry::new();
401 let result = registry.try_resolve(&scenario);
402 assert!(result.is_some());
403 assert_eq!(result.unwrap().confidence, Confidence::High);
404 }
405
406 #[test]
407 fn test_identical_change() {
408 let scenario = MergeScenario::new("old", "new", "new");
409 let registry = PatternRegistry::new();
410 let result = registry.try_resolve(&scenario);
411 assert!(result.is_some());
412 assert_eq!(result.unwrap().content, "new");
413 }
414
415 #[test]
416 fn test_both_add_lines() {
417 let scenario = MergeScenario::new("", "line_a", "line_b");
418 let registry = PatternRegistry::new();
419 let result = registry.try_resolve(&scenario);
420 assert!(result.is_some());
421 assert!(result.unwrap().content.contains("line_a"));
422 }
423
424 #[test]
425 fn test_import_union() {
426 let scenario = MergeScenario::new(
427 "import a\nimport b",
428 "import a\nimport b\nimport c",
429 "import a\nimport b\nimport d",
430 );
431 let registry = PatternRegistry::new();
432 let result = registry.try_resolve(&scenario);
433 assert!(result.is_some());
434 let content = result.unwrap().content;
435 assert!(content.contains("import c"));
436 assert!(content.contains("import d"));
437 }
438
439 #[test]
440 fn test_adjacent_edit() {
441 let scenario = MergeScenario::new(
442 "line1\nline2\nline3",
443 "modified1\nline2\nline3",
444 "line1\nline2\nmodified3",
445 );
446 let registry = PatternRegistry::new();
447 let result = registry.try_resolve(&scenario);
448 assert!(result.is_some());
449 let content = result.unwrap().content;
450 assert!(content.contains("modified1"));
451 assert!(content.contains("modified3"));
452 }
453
454 #[test]
455 fn test_prefix_suffix() {
456 let scenario = MergeScenario::new("base", "extended_base", "extended_base_more");
457 let registry = PatternRegistry::new();
458 let result = registry.try_resolve(&scenario);
459 assert!(result.is_some());
460 assert!(result.unwrap().content.contains("extended_base_more"));
461 }
462}