use crate::types::{Confidence, MergeScenario, ResolutionCandidate, ResolutionStrategy};
pub trait PatternRule: Send + Sync {
fn name(&self) -> &str;
fn matches(&self, scenario: &MergeScenario<&str>) -> bool;
fn resolve(&self, scenario: &MergeScenario<&str>) -> String;
fn confidence(&self) -> Confidence;
}
pub struct PatternRegistry {
rules: Vec<Box<dyn PatternRule>>,
}
impl PatternRegistry {
pub fn new() -> Self {
Self {
rules: vec![
Box::new(WhitespaceOnlyRule),
Box::new(IdenticalChangeRule),
Box::new(BothAddLinesRule),
Box::new(OneEmptyRule),
Box::new(PrefixSuffixRule),
Box::new(ImportUnionRule),
Box::new(AdjacentEditRule),
],
}
}
pub fn try_resolve(&self, scenario: &MergeScenario<&str>) -> Option<ResolutionCandidate> {
for rule in &self.rules {
if rule.matches(scenario) {
return Some(ResolutionCandidate {
content: rule.resolve(scenario),
confidence: rule.confidence(),
strategy: ResolutionStrategy::PatternRule,
});
}
}
None
}
pub fn try_resolve_all(&self, scenario: &MergeScenario<&str>) -> Vec<ResolutionCandidate> {
self.rules
.iter()
.filter(|rule| rule.matches(scenario))
.map(|rule| ResolutionCandidate {
content: rule.resolve(scenario),
confidence: rule.confidence(),
strategy: ResolutionStrategy::PatternRule,
})
.collect()
}
}
impl Default for PatternRegistry {
fn default() -> Self {
Self::new()
}
}
struct WhitespaceOnlyRule;
impl PatternRule for WhitespaceOnlyRule {
fn name(&self) -> &str {
"whitespace-only"
}
fn matches(&self, scenario: &MergeScenario<&str>) -> bool {
let base_norm = normalize_whitespace(scenario.base);
let left_norm = normalize_whitespace(scenario.left);
let right_norm = normalize_whitespace(scenario.right);
base_norm == left_norm && base_norm == right_norm || left_norm == right_norm
}
fn resolve(&self, scenario: &MergeScenario<&str>) -> String {
scenario.left.to_string()
}
fn confidence(&self) -> Confidence {
Confidence::High
}
}
struct IdenticalChangeRule;
impl PatternRule for IdenticalChangeRule {
fn name(&self) -> &str {
"identical-change"
}
fn matches(&self, scenario: &MergeScenario<&str>) -> bool {
scenario.left == scenario.right && scenario.left != scenario.base
}
fn resolve(&self, scenario: &MergeScenario<&str>) -> String {
scenario.left.to_string()
}
fn confidence(&self) -> Confidence {
Confidence::High
}
}
struct BothAddLinesRule;
impl PatternRule for BothAddLinesRule {
fn name(&self) -> &str {
"both-add-lines"
}
fn matches(&self, scenario: &MergeScenario<&str>) -> bool {
let base = scenario.base.trim();
if !base.is_empty() {
return false;
}
!scenario.left.trim().is_empty() && !scenario.right.trim().is_empty()
}
fn resolve(&self, scenario: &MergeScenario<&str>) -> String {
let mut result = scenario.left.to_string();
if !result.ends_with('\n') {
result.push('\n');
}
result.push_str(scenario.right);
result
}
fn confidence(&self) -> Confidence {
Confidence::Medium
}
}
struct OneEmptyRule;
impl PatternRule for OneEmptyRule {
fn name(&self) -> &str {
"one-empty"
}
fn matches(&self, scenario: &MergeScenario<&str>) -> bool {
let left_empty = scenario.left.trim().is_empty();
let right_empty = scenario.right.trim().is_empty();
(left_empty && !right_empty) || (!left_empty && right_empty)
}
fn resolve(&self, scenario: &MergeScenario<&str>) -> String {
if scenario.left.trim().is_empty() {
scenario.right.to_string()
} else {
scenario.left.to_string()
}
}
fn confidence(&self) -> Confidence {
Confidence::Medium
}
}
struct PrefixSuffixRule;
impl PatternRule for PrefixSuffixRule {
fn name(&self) -> &str {
"prefix-suffix"
}
fn matches(&self, scenario: &MergeScenario<&str>) -> bool {
let left = scenario.left.trim();
let right = scenario.right.trim();
if left == right {
return false;
}
left.starts_with(right)
|| right.starts_with(left)
|| left.ends_with(right)
|| right.ends_with(left)
}
fn resolve(&self, scenario: &MergeScenario<&str>) -> String {
let left = scenario.left.trim();
let right = scenario.right.trim();
if left.len() >= right.len() {
scenario.left.to_string()
} else {
scenario.right.to_string()
}
}
fn confidence(&self) -> Confidence {
Confidence::Medium
}
}
struct ImportUnionRule;
impl PatternRule for ImportUnionRule {
fn name(&self) -> &str {
"import-union"
}
fn matches(&self, scenario: &MergeScenario<&str>) -> bool {
let all_imports = |text: &str| {
text.lines()
.filter(|l| !l.trim().is_empty())
.all(is_import_line)
};
all_imports(scenario.base) && all_imports(scenario.left) && all_imports(scenario.right)
}
fn resolve(&self, scenario: &MergeScenario<&str>) -> String {
let mut imports: Vec<String> = Vec::new();
for line in scenario.left.lines().chain(scenario.right.lines()) {
let trimmed = line.trim().to_string();
if !trimmed.is_empty() && !imports.contains(&trimmed) {
imports.push(trimmed);
}
}
imports.sort();
imports.join("\n")
}
fn confidence(&self) -> Confidence {
Confidence::High
}
}
struct AdjacentEditRule;
impl PatternRule for AdjacentEditRule {
fn name(&self) -> &str {
"adjacent-edit"
}
fn matches(&self, scenario: &MergeScenario<&str>) -> bool {
let base_lines: Vec<&str> = scenario.base.lines().collect();
let left_lines: Vec<&str> = scenario.left.lines().collect();
let right_lines: Vec<&str> = scenario.right.lines().collect();
if base_lines.len() != left_lines.len() || base_lines.len() != right_lines.len() {
return false;
}
for i in 0..base_lines.len() {
let left_changed = base_lines[i] != left_lines[i];
let right_changed = base_lines[i] != right_lines[i];
if left_changed && right_changed {
return false;
}
}
let left_has_changes = base_lines
.iter()
.zip(left_lines.iter())
.any(|(b, l)| b != l);
let right_has_changes = base_lines
.iter()
.zip(right_lines.iter())
.any(|(b, r)| b != r);
left_has_changes && right_has_changes
}
fn resolve(&self, scenario: &MergeScenario<&str>) -> String {
let base_lines: Vec<&str> = scenario.base.lines().collect();
let left_lines: Vec<&str> = scenario.left.lines().collect();
let right_lines: Vec<&str> = scenario.right.lines().collect();
let mut result = Vec::new();
for i in 0..base_lines.len() {
if base_lines[i] != left_lines[i] {
result.push(left_lines[i]);
} else if base_lines[i] != right_lines[i] {
result.push(right_lines[i]);
} else {
result.push(base_lines[i]);
}
}
result.join("\n")
}
fn confidence(&self) -> Confidence {
Confidence::High
}
}
fn normalize_whitespace(s: &str) -> String {
s.split_whitespace().collect::<Vec<_>>().join(" ")
}
fn is_import_line(line: &str) -> bool {
let trimmed = line.trim();
trimmed.starts_with("import ")
|| trimmed.starts_with("from ")
|| trimmed.starts_with("use ")
|| trimmed.starts_with("#include")
|| trimmed.starts_with("require(")
|| trimmed.starts_with("const ")
&& (trimmed.contains("require(") || trimmed.contains("import("))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_whitespace_only() {
let scenario = MergeScenario::new(
"int x = 1;",
"int x = 1;", "int x = 1;", );
let registry = PatternRegistry::new();
let result = registry.try_resolve(&scenario);
assert!(result.is_some());
assert_eq!(result.unwrap().confidence, Confidence::High);
}
#[test]
fn test_identical_change() {
let scenario = MergeScenario::new("old", "new", "new");
let registry = PatternRegistry::new();
let result = registry.try_resolve(&scenario);
assert!(result.is_some());
assert_eq!(result.unwrap().content, "new");
}
#[test]
fn test_both_add_lines() {
let scenario = MergeScenario::new("", "line_a", "line_b");
let registry = PatternRegistry::new();
let result = registry.try_resolve(&scenario);
assert!(result.is_some());
assert!(result.unwrap().content.contains("line_a"));
}
#[test]
fn test_import_union() {
let scenario = MergeScenario::new(
"import a\nimport b",
"import a\nimport b\nimport c",
"import a\nimport b\nimport d",
);
let registry = PatternRegistry::new();
let result = registry.try_resolve(&scenario);
assert!(result.is_some());
let content = result.unwrap().content;
assert!(content.contains("import c"));
assert!(content.contains("import d"));
}
#[test]
fn test_adjacent_edit() {
let scenario = MergeScenario::new(
"line1\nline2\nline3",
"modified1\nline2\nline3",
"line1\nline2\nmodified3",
);
let registry = PatternRegistry::new();
let result = registry.try_resolve(&scenario);
assert!(result.is_some());
let content = result.unwrap().content;
assert!(content.contains("modified1"));
assert!(content.contains("modified3"));
}
#[test]
fn test_prefix_suffix() {
let scenario = MergeScenario::new("base", "extended_base", "extended_base_more");
let registry = PatternRegistry::new();
let result = registry.try_resolve(&scenario);
assert!(result.is_some());
assert!(result.unwrap().content.contains("extended_base_more"));
}
}