1use std::collections::{HashMap, HashSet};
9use std::path::Path;
10
11use dk_core::{FileAnalysis, Symbol};
12
13use crate::parser::ParserRegistry;
14
15#[derive(Debug, Clone)]
19pub struct SemanticConflict {
20 pub file_path: String,
22 pub symbol_name: String,
24 pub our_change: SymbolChangeKind,
26 pub their_change: SymbolChangeKind,
28}
29
30#[derive(Debug, Clone, PartialEq, Eq)]
32pub enum SymbolChangeKind {
33 Added,
34 Modified,
35 Removed,
36}
37
38#[derive(Debug)]
40pub enum MergeAnalysis {
41 AutoMerge {
43 merged_content: Vec<u8>,
45 },
46 Conflict {
48 conflicts: Vec<SemanticConflict>,
49 },
50}
51
52pub fn analyze_file_conflict(
64 file_path: &str,
65 base_content: &[u8],
66 head_content: &[u8],
67 overlay_content: &[u8],
68 parser: &ParserRegistry,
69) -> MergeAnalysis {
70 let path = Path::new(file_path);
71
72 let base_parse = parser.parse_file(path, base_content);
74 let head_parse = parser.parse_file(path, head_content);
75 let overlay_parse = parser.parse_file(path, overlay_content);
76
77 match (base_parse, head_parse, overlay_parse) {
78 (Ok(base_fa), Ok(head_fa), Ok(overlay_fa)) => {
79 semantic_analysis(file_path, &base_fa, &head_fa, &overlay_fa, overlay_content)
80 }
81 _ => {
82 byte_level_analysis(file_path, base_content, head_content, overlay_content)
84 }
85 }
86}
87
88fn semantic_analysis(
90 file_path: &str,
91 base: &FileAnalysis,
92 head: &FileAnalysis,
93 overlay: &FileAnalysis,
94 overlay_content: &[u8],
95) -> MergeAnalysis {
96 let base_syms = symbol_map(&base.symbols);
97 let head_syms = symbol_map(&head.symbols);
98 let overlay_syms = symbol_map(&overlay.symbols);
99
100 let all_names: HashSet<&str> = base_syms
102 .keys()
103 .chain(head_syms.keys())
104 .chain(overlay_syms.keys())
105 .copied()
106 .collect();
107
108 let mut conflicts = Vec::new();
109
110 for name in all_names {
111 let base_sym = base_syms.get(name);
112 let head_sym = head_syms.get(name);
113 let overlay_sym = overlay_syms.get(name);
114
115 let head_change = classify_change(base_sym, head_sym);
116 let overlay_change = classify_change(base_sym, overlay_sym);
117
118 if let (Some(their), Some(ours)) = (&head_change, &overlay_change) {
121 if their == ours {
123 let identical = match (head_sym, overlay_sym) {
125 (Some(h), Some(o)) => symbols_equivalent(h, o),
126 (None, None) => true, _ => false,
128 };
129 if identical {
130 continue; }
132 }
133
134 conflicts.push(SemanticConflict {
135 file_path: file_path.to_string(),
136 symbol_name: name.to_string(),
137 our_change: ours.clone(),
138 their_change: their.clone(),
139 });
140 }
141 }
142
143 if conflicts.is_empty() {
144 MergeAnalysis::AutoMerge {
145 merged_content: overlay_content.to_vec(),
146 }
147 } else {
148 MergeAnalysis::Conflict { conflicts }
149 }
150}
151
152fn byte_level_analysis(
154 file_path: &str,
155 base_content: &[u8],
156 head_content: &[u8],
157 overlay_content: &[u8],
158) -> MergeAnalysis {
159 let head_changed = base_content != head_content;
160 let overlay_changed = base_content != overlay_content;
161
162 if head_changed && overlay_changed && head_content != overlay_content {
163 MergeAnalysis::Conflict {
165 conflicts: vec![SemanticConflict {
166 file_path: file_path.to_string(),
167 symbol_name: "<entire file>".to_string(),
168 our_change: SymbolChangeKind::Modified,
169 their_change: SymbolChangeKind::Modified,
170 }],
171 }
172 } else {
173 MergeAnalysis::AutoMerge {
176 merged_content: if overlay_changed {
177 overlay_content.to_vec()
178 } else {
179 head_content.to_vec()
180 },
181 }
182 }
183}
184
185fn symbol_map(symbols: &[Symbol]) -> HashMap<&str, &Symbol> {
187 symbols
188 .iter()
189 .map(|s| (s.qualified_name.as_str(), s))
190 .collect()
191}
192
193fn classify_change(
195 base: Option<&&Symbol>,
196 current: Option<&&Symbol>,
197) -> Option<SymbolChangeKind> {
198 match (base, current) {
199 (None, None) => None,
200 (None, Some(_)) => Some(SymbolChangeKind::Added),
201 (Some(_), None) => Some(SymbolChangeKind::Removed),
202 (Some(b), Some(c)) => {
203 if symbols_equivalent(b, c) {
204 None } else {
206 Some(SymbolChangeKind::Modified)
207 }
208 }
209 }
210}
211
212fn symbols_equivalent(a: &Symbol, b: &Symbol) -> bool {
215 a.qualified_name == b.qualified_name
216 && a.kind == b.kind
217 && a.visibility == b.visibility
218 && a.span == b.span
219 && a.signature == b.signature
220}
221
222#[cfg(test)]
223mod tests {
224 use super::*;
225
226 #[test]
227 fn byte_level_no_conflict_when_only_overlay_changed() {
228 let base = b"base content";
229 let head = b"base content"; let overlay = b"overlay content";
231
232 match byte_level_analysis("test.txt", base, head, overlay) {
233 MergeAnalysis::AutoMerge { merged_content } => {
234 assert_eq!(merged_content, overlay.to_vec());
235 }
236 MergeAnalysis::Conflict { .. } => panic!("expected auto-merge"),
237 }
238 }
239
240 #[test]
241 fn byte_level_no_conflict_when_only_head_changed() {
242 let base = b"base content";
243 let head = b"head content";
244 let overlay = b"base content"; match byte_level_analysis("test.txt", base, head, overlay) {
247 MergeAnalysis::AutoMerge { merged_content } => {
248 assert_eq!(merged_content, head.to_vec());
249 }
250 MergeAnalysis::Conflict { .. } => panic!("expected auto-merge"),
251 }
252 }
253
254 #[test]
255 fn byte_level_conflict_when_both_changed_differently() {
256 let base = b"base content";
257 let head = b"head content";
258 let overlay = b"overlay content";
259
260 match byte_level_analysis("test.txt", base, head, overlay) {
261 MergeAnalysis::Conflict { conflicts } => {
262 assert_eq!(conflicts.len(), 1);
263 assert_eq!(conflicts[0].symbol_name, "<entire file>");
264 }
265 MergeAnalysis::AutoMerge { .. } => panic!("expected conflict"),
266 }
267 }
268
269 #[test]
270 fn byte_level_no_conflict_when_both_changed_identically() {
271 let base = b"base content";
272 let same = b"same content";
273
274 match byte_level_analysis("test.txt", base, same, same) {
275 MergeAnalysis::AutoMerge { .. } => {} MergeAnalysis::Conflict { .. } => panic!("expected auto-merge"),
277 }
278 }
279
280 #[test]
281 fn classify_change_cases() {
282 use dk_core::{Span, SymbolKind, Visibility};
283 use std::path::PathBuf;
284 use uuid::Uuid;
285
286 let sym = Symbol {
287 id: Uuid::new_v4(),
288 name: "f".into(),
289 qualified_name: "f".into(),
290 kind: SymbolKind::Function,
291 visibility: Visibility::Public,
292 file_path: PathBuf::from("t.rs"),
293 span: Span {
294 start_byte: 0,
295 end_byte: 10,
296 },
297 signature: None,
298 doc_comment: None,
299 parent: None,
300 last_modified_by: None,
301 last_modified_intent: None,
302 };
303
304 assert!(classify_change(None, None).is_none());
306
307 assert_eq!(
309 classify_change(None, Some(&&sym)),
310 Some(SymbolChangeKind::Added)
311 );
312
313 assert_eq!(
315 classify_change(Some(&&sym), None),
316 Some(SymbolChangeKind::Removed)
317 );
318
319 assert!(classify_change(Some(&&sym), Some(&&sym)).is_none());
321 }
322}