1use std::path::Path;
9
10use serde::Serialize;
11
12use dk_core::{Error, Result, Symbol};
13
14use crate::parser::ParserRegistry;
15
16#[derive(Debug, Clone, Serialize)]
18pub struct ConflictBlock {
19 pub conflicting_symbols: Vec<SymbolConflictDetail>,
20 pub message: String,
21}
22
23#[derive(Debug, Clone, Serialize)]
25pub struct SymbolConflictDetail {
26 pub file_path: String,
27 pub qualified_name: String,
28 pub kind: String,
29 pub conflicting_agent: String,
30 pub their_change: SymbolVersion,
31 pub your_change: SymbolVersion,
32 pub base_version: SymbolVersion,
33}
34
35#[derive(Debug, Clone, Serialize)]
37pub struct SymbolVersion {
38 pub description: String,
39 pub signature: String,
40 pub body: String,
41 pub change_type: String,
43}
44
45fn find_symbol_in_content(
47 registry: &ParserRegistry,
48 file_path: &str,
49 content: &str,
50 qualified_name: &str,
51) -> Result<Option<(Symbol, String)>> {
52 let path = Path::new(file_path);
53 if !registry.supports_file(path) {
54 return Err(Error::UnsupportedLanguage(format!(
55 "Unsupported file: {file_path}"
56 )));
57 }
58
59 let analysis = registry.parse_file(path, content.as_bytes())?;
60
61 for sym in &analysis.symbols {
62 if sym.qualified_name == qualified_name {
63 let start = sym.span.start_byte as usize;
64 let end = sym.span.end_byte as usize;
65 let bytes = content.as_bytes();
66 if end <= bytes.len() {
67 let text = String::from_utf8_lossy(&bytes[start..end])
68 .replace('\0', "");
69 return Ok(Some((sym.clone(), text)));
70 }
71 }
72 }
73
74 Ok(None)
75}
76
77fn extract_signature(source: &str) -> String {
79 source
80 .lines()
81 .next()
82 .unwrap_or("")
83 .trim()
84 .to_string()
85}
86
87fn line_count(source: &str) -> usize {
89 if source.is_empty() {
90 0
91 } else {
92 source.lines().count()
93 }
94}
95
96fn describe_change(
98 base_sig: &str,
99 base_body: &str,
100 changed_sig: &str,
101 changed_body: &str,
102 change_type: &str,
103) -> String {
104 match change_type {
105 "added" => "New symbol added".to_string(),
106 "deleted" => "Symbol deleted".to_string(),
107 _ => {
108 let mut parts = Vec::new();
109
110 if base_sig != changed_sig {
111 parts.push(format!(
112 "Signature changed from `{base_sig}` to `{changed_sig}`"
113 ));
114 }
115
116 let base_lines = line_count(base_body);
117 let changed_lines = line_count(changed_body);
118 if changed_lines > base_lines {
119 parts.push(format!(
120 "Added {} lines",
121 changed_lines - base_lines
122 ));
123 } else if changed_lines < base_lines {
124 parts.push(format!(
125 "Removed {} lines",
126 base_lines - changed_lines
127 ));
128 } else if base_body != changed_body {
129 parts.push("Body modified (same line count)".to_string());
130 }
131
132 if parts.is_empty() {
133 "No visible changes".to_string()
134 } else {
135 parts.join("; ")
136 }
137 }
138 }
139}
140
141fn build_symbol_version(
144 registry: &ParserRegistry,
145 file_path: &str,
146 content: &str,
147 qualified_name: &str,
148 base_sig: &str,
149 base_body: &str,
150 label: &str,
151) -> Result<SymbolVersion> {
152 match find_symbol_in_content(registry, file_path, content, qualified_name)? {
153 Some((_sym, text)) => {
154 let sig = extract_signature(&text);
155 let change_type = if label == "base" {
156 "base".to_string()
157 } else if base_body.is_empty() {
158 "added".to_string()
159 } else {
160 "modified".to_string()
161 };
162 let desc = describe_change(base_sig, base_body, &sig, &text, &change_type);
163 Ok(SymbolVersion {
164 description: desc,
165 signature: sig,
166 body: text,
167 change_type,
168 })
169 }
170 None => {
171 let change_type = if label == "base" {
172 "base".to_string()
173 } else {
174 "deleted".to_string()
175 };
176 let desc = describe_change(base_sig, base_body, "", "", &change_type);
177 Ok(SymbolVersion {
178 description: desc,
179 signature: String::new(),
180 body: String::new(),
181 change_type,
182 })
183 }
184 }
185}
186
187pub fn build_conflict_detail(
196 registry: &ParserRegistry,
197 file_path: &str,
198 qualified_name: &str,
199 conflicting_agent: &str,
200 base_content: &str,
201 their_content: &str,
202 your_content: &str,
203) -> Result<SymbolConflictDetail> {
204 let (base_sig, base_body, kind) =
206 match find_symbol_in_content(registry, file_path, base_content, qualified_name)? {
207 Some((sym, text)) => {
208 let sig = extract_signature(&text);
209 let kind = sym.kind.to_string();
210 (sig, text, kind)
211 }
212 None => (String::new(), String::new(), "unknown".to_string()),
213 };
214
215 let base_version = SymbolVersion {
216 description: if base_body.is_empty() {
217 "Symbol does not exist in base".to_string()
218 } else {
219 "Base version".to_string()
220 },
221 signature: base_sig.clone(),
222 body: base_body.clone(),
223 change_type: "base".to_string(),
224 };
225
226 let their_change = build_symbol_version(
227 registry,
228 file_path,
229 their_content,
230 qualified_name,
231 &base_sig,
232 &base_body,
233 "their",
234 )?;
235
236 let your_change = build_symbol_version(
237 registry,
238 file_path,
239 your_content,
240 qualified_name,
241 &base_sig,
242 &base_body,
243 "your",
244 )?;
245
246 Ok(SymbolConflictDetail {
247 file_path: file_path.to_string(),
248 qualified_name: qualified_name.to_string(),
249 kind,
250 conflicting_agent: conflicting_agent.to_string(),
251 their_change,
252 your_change,
253 base_version,
254 })
255}
256
257pub fn build_conflict_block(
262 registry: &ParserRegistry,
263 conflicts: &[(
264 &str, &str, &str, &str, &str, &str, )],
271) -> Result<ConflictBlock> {
272 let mut details = Vec::new();
273
274 for (file_path, qualified_name, agent, base, theirs, yours) in conflicts {
275 details.push(build_conflict_detail(
276 registry,
277 file_path,
278 qualified_name,
279 agent,
280 base,
281 theirs,
282 yours,
283 )?);
284 }
285
286 let count = details.len();
287 let message = if count == 1 {
288 format!(
289 "1 symbol conflict detected in {}",
290 details[0].file_path
291 )
292 } else {
293 let files: std::collections::BTreeSet<&str> =
294 details.iter().map(|d| d.file_path.as_str()).collect();
295 format!(
296 "{count} symbol conflicts detected across {} file(s)",
297 files.len()
298 )
299 };
300
301 Ok(ConflictBlock {
302 conflicting_symbols: details,
303 message,
304 })
305}