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]).to_string();
68 return Ok(Some((sym.clone(), text)));
69 }
70 }
71 }
72
73 Ok(None)
74}
75
76fn extract_signature(source: &str) -> String {
78 source
79 .lines()
80 .next()
81 .unwrap_or("")
82 .trim()
83 .to_string()
84}
85
86fn line_count(source: &str) -> usize {
88 if source.is_empty() {
89 0
90 } else {
91 source.lines().count()
92 }
93}
94
95fn describe_change(
97 base_sig: &str,
98 base_body: &str,
99 changed_sig: &str,
100 changed_body: &str,
101 change_type: &str,
102) -> String {
103 match change_type {
104 "added" => "New symbol added".to_string(),
105 "deleted" => "Symbol deleted".to_string(),
106 _ => {
107 let mut parts = Vec::new();
108
109 if base_sig != changed_sig {
110 parts.push(format!(
111 "Signature changed from `{base_sig}` to `{changed_sig}`"
112 ));
113 }
114
115 let base_lines = line_count(base_body);
116 let changed_lines = line_count(changed_body);
117 if changed_lines > base_lines {
118 parts.push(format!(
119 "Added {} lines",
120 changed_lines - base_lines
121 ));
122 } else if changed_lines < base_lines {
123 parts.push(format!(
124 "Removed {} lines",
125 base_lines - changed_lines
126 ));
127 } else if base_body != changed_body {
128 parts.push("Body modified (same line count)".to_string());
129 }
130
131 if parts.is_empty() {
132 "No visible changes".to_string()
133 } else {
134 parts.join("; ")
135 }
136 }
137 }
138}
139
140fn build_symbol_version(
143 registry: &ParserRegistry,
144 file_path: &str,
145 content: &str,
146 qualified_name: &str,
147 base_sig: &str,
148 base_body: &str,
149 label: &str,
150) -> Result<SymbolVersion> {
151 match find_symbol_in_content(registry, file_path, content, qualified_name)? {
152 Some((_sym, text)) => {
153 let sig = extract_signature(&text);
154 let change_type = if label == "base" {
155 "base".to_string()
156 } else if base_body.is_empty() {
157 "added".to_string()
158 } else {
159 "modified".to_string()
160 };
161 let desc = describe_change(base_sig, base_body, &sig, &text, &change_type);
162 Ok(SymbolVersion {
163 description: desc,
164 signature: sig,
165 body: text,
166 change_type,
167 })
168 }
169 None => {
170 let change_type = if label == "base" {
171 "base".to_string()
172 } else {
173 "deleted".to_string()
174 };
175 let desc = describe_change(base_sig, base_body, "", "", &change_type);
176 Ok(SymbolVersion {
177 description: desc,
178 signature: String::new(),
179 body: String::new(),
180 change_type,
181 })
182 }
183 }
184}
185
186pub fn build_conflict_detail(
195 registry: &ParserRegistry,
196 file_path: &str,
197 qualified_name: &str,
198 conflicting_agent: &str,
199 base_content: &str,
200 their_content: &str,
201 your_content: &str,
202) -> Result<SymbolConflictDetail> {
203 let (base_sig, base_body, kind) =
205 match find_symbol_in_content(registry, file_path, base_content, qualified_name)? {
206 Some((sym, text)) => {
207 let sig = extract_signature(&text);
208 let kind = sym.kind.to_string();
209 (sig, text, kind)
210 }
211 None => (String::new(), String::new(), "unknown".to_string()),
212 };
213
214 let base_version = SymbolVersion {
215 description: if base_body.is_empty() {
216 "Symbol does not exist in base".to_string()
217 } else {
218 "Base version".to_string()
219 },
220 signature: base_sig.clone(),
221 body: base_body.clone(),
222 change_type: "base".to_string(),
223 };
224
225 let their_change = build_symbol_version(
226 registry,
227 file_path,
228 their_content,
229 qualified_name,
230 &base_sig,
231 &base_body,
232 "their",
233 )?;
234
235 let your_change = build_symbol_version(
236 registry,
237 file_path,
238 your_content,
239 qualified_name,
240 &base_sig,
241 &base_body,
242 "your",
243 )?;
244
245 Ok(SymbolConflictDetail {
246 file_path: file_path.to_string(),
247 qualified_name: qualified_name.to_string(),
248 kind,
249 conflicting_agent: conflicting_agent.to_string(),
250 their_change,
251 your_change,
252 base_version,
253 })
254}
255
256pub fn build_conflict_block(
261 registry: &ParserRegistry,
262 conflicts: &[(
263 &str, &str, &str, &str, &str, &str, )],
270) -> Result<ConflictBlock> {
271 let mut details = Vec::new();
272
273 for (file_path, qualified_name, agent, base, theirs, yours) in conflicts {
274 details.push(build_conflict_detail(
275 registry,
276 file_path,
277 qualified_name,
278 agent,
279 base,
280 theirs,
281 yours,
282 )?);
283 }
284
285 let count = details.len();
286 let message = if count == 1 {
287 format!(
288 "1 symbol conflict detected in {}",
289 details[0].file_path
290 )
291 } else {
292 let files: std::collections::BTreeSet<&str> =
293 details.iter().map(|d| d.file_path.as_str()).collect();
294 format!(
295 "{count} symbol conflicts detected across {} file(s)",
296 files.len()
297 )
298 };
299
300 Ok(ConflictBlock {
301 conflicting_symbols: details,
302 message,
303 })
304}