1use std::collections::HashMap;
5
6use serde::Serialize;
7
8use crate::model::{ExtractedSymbol, SymbolKind, SymbolWithParent};
9
10pub const BUDGET_GUARD_THRESHOLD: usize = 5;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
15#[serde(rename_all = "snake_case")]
16pub enum ChangeType {
17 Added,
18 Modified,
19 Removed,
20}
21
22#[derive(Debug, Clone, Serialize)]
24pub struct SymbolChange {
25 pub name: String,
26 pub kind: SymbolKind,
27 pub change_type: ChangeType,
28 pub file_path: String,
29 pub line: u32,
30 pub parent_name: Option<String>,
31 pub old_signature: Option<String>,
32 pub new_signature: Option<String>,
33}
34
35#[derive(Hash, Eq, PartialEq)]
37pub(crate) struct SymbolKey {
38 pub name: String,
39 pub kind: SymbolKind,
40 pub parent_name: Option<String>,
41}
42
43pub(crate) fn resolve_parent_name(
45 symbols: &[ExtractedSymbol],
46 sym: &ExtractedSymbol,
47) -> Option<String> {
48 sym.parent_index
49 .and_then(|idx| symbols.get(idx).map(|p| p.name.clone()))
50}
51
52pub(crate) fn symbol_change_from_current(
54 sym: SymbolWithParent,
55 change_type: ChangeType,
56) -> SymbolChange {
57 SymbolChange {
58 name: sym.name,
59 kind: sym.kind,
60 change_type,
61 file_path: sym.file_path,
62 line: sym.start_line as u32,
63 parent_name: sym.parent_name,
64 old_signature: None,
65 new_signature: sym.signature,
66 }
67}
68
69pub(crate) fn symbol_change_from_old(
71 sym: &ExtractedSymbol,
72 all_old: &[ExtractedSymbol],
73 file_path: &str,
74) -> SymbolChange {
75 SymbolChange {
76 name: sym.name.clone(),
77 kind: sym.kind,
78 change_type: ChangeType::Removed,
79 file_path: file_path.to_string(),
80 line: sym.start_line as u32,
81 parent_name: resolve_parent_name(all_old, sym),
82 old_signature: sym.signature.clone(),
83 new_signature: None,
84 }
85}
86
87pub fn compare_extracted_symbols(
92 old_symbols: &[ExtractedSymbol],
93 current_symbols: &[SymbolWithParent],
94 file_path: &str,
95) -> (Vec<SymbolChange>, Vec<i64>) {
96 let mut old_map: HashMap<SymbolKey, Vec<&ExtractedSymbol>> = HashMap::new();
98 for sym in old_symbols {
99 let key = SymbolKey {
100 name: sym.name.clone(),
101 kind: sym.kind,
102 parent_name: resolve_parent_name(old_symbols, sym),
103 };
104 old_map.entry(key).or_default().push(sym);
105 }
106
107 let mut current_map: HashMap<SymbolKey, Vec<&SymbolWithParent>> = HashMap::new();
108 for sym in current_symbols {
109 let key = SymbolKey {
110 name: sym.name.clone(),
111 kind: sym.kind,
112 parent_name: sym.parent_name.clone(),
113 };
114 current_map.entry(key).or_default().push(sym);
115 }
116
117 let mut changes = Vec::new();
118 let mut modified_ids = Vec::new();
119
120 for (key, cur_syms) in ¤t_map {
122 if let Some(old_syms) = old_map.get(key) {
123 let paired = old_syms.len().min(cur_syms.len());
124 for i in 0..paired {
125 if old_syms[i].signature != cur_syms[i].signature {
126 changes.push(SymbolChange {
127 name: cur_syms[i].name.clone(),
128 kind: cur_syms[i].kind,
129 change_type: ChangeType::Modified,
130 file_path: file_path.to_string(),
131 line: cur_syms[i].start_line as u32,
132 parent_name: cur_syms[i].parent_name.clone(),
133 old_signature: old_syms[i].signature.clone(),
134 new_signature: cur_syms[i].signature.clone(),
135 });
136 modified_ids.push(cur_syms[i].id);
137 }
138 }
139 for cur in cur_syms.iter().skip(paired) {
140 changes.push(SymbolChange {
141 name: cur.name.clone(),
142 kind: cur.kind,
143 change_type: ChangeType::Added,
144 file_path: file_path.to_string(),
145 line: cur.start_line as u32,
146 parent_name: cur.parent_name.clone(),
147 old_signature: None,
148 new_signature: cur.signature.clone(),
149 });
150 }
151 } else {
152 for cur in cur_syms {
153 changes.push(SymbolChange {
154 name: cur.name.clone(),
155 kind: cur.kind,
156 change_type: ChangeType::Added,
157 file_path: file_path.to_string(),
158 line: cur.start_line as u32,
159 parent_name: cur.parent_name.clone(),
160 old_signature: None,
161 new_signature: cur.signature.clone(),
162 });
163 }
164 }
165 }
166
167 for (key, old_syms) in &old_map {
169 let paired = current_map
170 .get(key)
171 .map_or(0, |c| c.len().min(old_syms.len()));
172 for old in old_syms.iter().skip(paired) {
173 changes.push(symbol_change_from_old(old, old_symbols, file_path));
174 }
175 }
176
177 if changes.len() > BUDGET_GUARD_THRESHOLD {
179 modified_ids.clear();
180 }
181
182 (changes, modified_ids)
183}
184
185pub fn compare_db_symbols(
190 old_symbols: &[SymbolWithParent],
191 current_symbols: &[SymbolWithParent],
192 file_path: &str,
193) -> (Vec<SymbolChange>, Vec<i64>) {
194 let mut old_map: HashMap<SymbolKey, Vec<&SymbolWithParent>> = HashMap::new();
195 for sym in old_symbols {
196 let key = SymbolKey {
197 name: sym.name.clone(),
198 kind: sym.kind,
199 parent_name: sym.parent_name.clone(),
200 };
201 old_map.entry(key).or_default().push(sym);
202 }
203
204 let mut current_map: HashMap<SymbolKey, Vec<&SymbolWithParent>> = HashMap::new();
205 for sym in current_symbols {
206 let key = SymbolKey {
207 name: sym.name.clone(),
208 kind: sym.kind,
209 parent_name: sym.parent_name.clone(),
210 };
211 current_map.entry(key).or_default().push(sym);
212 }
213
214 let mut changes = Vec::new();
215 let mut modified_ids = Vec::new();
216
217 for (key, cur_syms) in ¤t_map {
218 if let Some(old_syms) = old_map.get(key) {
219 let paired = old_syms.len().min(cur_syms.len());
220 for i in 0..paired {
221 if old_syms[i].signature != cur_syms[i].signature {
222 changes.push(SymbolChange {
223 name: cur_syms[i].name.clone(),
224 kind: cur_syms[i].kind,
225 change_type: ChangeType::Modified,
226 file_path: file_path.to_string(),
227 line: cur_syms[i].start_line as u32,
228 parent_name: cur_syms[i].parent_name.clone(),
229 old_signature: old_syms[i].signature.clone(),
230 new_signature: cur_syms[i].signature.clone(),
231 });
232 modified_ids.push(cur_syms[i].id);
233 }
234 }
235 for cur in cur_syms.iter().skip(paired) {
236 changes.push(symbol_change_from_current(
237 (*cur).clone(),
238 ChangeType::Added,
239 ));
240 }
241 } else {
242 for cur in cur_syms {
243 changes.push(symbol_change_from_current(
244 (*cur).clone(),
245 ChangeType::Added,
246 ));
247 }
248 }
249 }
250
251 for (key, old_syms) in &old_map {
252 let paired = current_map
253 .get(key)
254 .map_or(0, |c| c.len().min(old_syms.len()));
255 for old in old_syms.iter().skip(paired) {
256 changes.push(SymbolChange {
257 name: old.name.clone(),
258 kind: old.kind,
259 change_type: ChangeType::Removed,
260 file_path: file_path.to_string(),
261 line: old.start_line as u32,
262 parent_name: old.parent_name.clone(),
263 old_signature: old.signature.clone(),
264 new_signature: None,
265 });
266 }
267 }
268
269 if changes.len() > BUDGET_GUARD_THRESHOLD {
270 modified_ids.clear();
271 }
272
273 (changes, modified_ids)
274}
275
276#[cfg(test)]
277mod tests {
278 use super::*;
279
280 fn make_extracted(
281 name: &str,
282 kind: SymbolKind,
283 sig: Option<&str>,
284 parent_index: Option<usize>,
285 ) -> ExtractedSymbol {
286 ExtractedSymbol {
287 name: name.to_string(),
288 kind,
289 signature: sig.map(|s| s.to_string()),
290 start_line: 1,
291 end_line: 5,
292 start_byte: 0,
293 end_byte: 100,
294 parent_index,
295 }
296 }
297
298 fn make_db_sym(
299 id: i64,
300 name: &str,
301 kind: SymbolKind,
302 sig: Option<&str>,
303 parent_name: Option<&str>,
304 ) -> SymbolWithParent {
305 SymbolWithParent {
306 id,
307 name: name.to_string(),
308 kind,
309 file_path: "src/lib.rs".to_string(),
310 start_line: 1,
311 end_line: 5,
312 signature: sig.map(|s| s.to_string()),
313 parent_name: parent_name.map(|s| s.to_string()),
314 }
315 }
316
317 #[test]
320 fn extracted_added_new_key() {
321 let (changes, _) = compare_extracted_symbols(
322 &[],
323 &[make_db_sym(
324 1,
325 "new_fn",
326 SymbolKind::Function,
327 Some("fn new_fn()"),
328 None,
329 )],
330 "src/lib.rs",
331 );
332 assert_eq!(changes.len(), 1);
333 assert_eq!(changes[0].name, "new_fn");
334 assert_eq!(changes[0].change_type, ChangeType::Added);
335 }
336
337 #[test]
338 fn extracted_removed_key() {
339 let old = vec![make_extracted(
340 "old_fn",
341 SymbolKind::Function,
342 Some("fn old_fn()"),
343 None,
344 )];
345 let (changes, _) = compare_extracted_symbols(&old, &[], "src/lib.rs");
346 assert_eq!(changes.len(), 1);
347 assert_eq!(changes[0].name, "old_fn");
348 assert_eq!(changes[0].change_type, ChangeType::Removed);
349 }
350
351 #[test]
352 fn extracted_modified_signature() {
353 let old = vec![make_extracted(
354 "foo",
355 SymbolKind::Function,
356 Some("fn foo()"),
357 None,
358 )];
359 let cur = vec![make_db_sym(
360 1,
361 "foo",
362 SymbolKind::Function,
363 Some("fn foo(x: i32)"),
364 None,
365 )];
366 let (changes, mod_ids) = compare_extracted_symbols(&old, &cur, "src/lib.rs");
367 assert_eq!(changes.len(), 1);
368 assert_eq!(changes[0].change_type, ChangeType::Modified);
369 assert_eq!(changes[0].old_signature.as_deref(), Some("fn foo()"));
370 assert_eq!(changes[0].new_signature.as_deref(), Some("fn foo(x: i32)"));
371 assert_eq!(mod_ids, vec![1]);
372 }
373
374 #[test]
375 fn extracted_unchanged() {
376 let old = vec![make_extracted(
377 "foo",
378 SymbolKind::Function,
379 Some("fn foo()"),
380 None,
381 )];
382 let cur = vec![make_db_sym(
383 1,
384 "foo",
385 SymbolKind::Function,
386 Some("fn foo()"),
387 None,
388 )];
389 let (changes, _) = compare_extracted_symbols(&old, &cur, "src/lib.rs");
390 assert!(changes.is_empty());
391 }
392
393 #[test]
394 fn extracted_budget_guard() {
395 let old: Vec<ExtractedSymbol> = (0..6)
396 .map(|i| {
397 make_extracted(
398 &format!("fn_{i}"),
399 SymbolKind::Function,
400 Some(&format!("fn fn_{i}()")),
401 None,
402 )
403 })
404 .collect();
405 let (changes, mod_ids) = compare_extracted_symbols(&old, &[], "src/lib.rs");
406 assert_eq!(changes.len(), 6);
407 assert!(mod_ids.is_empty(), "budget guard should clear modified_ids");
408 }
409
410 #[test]
413 fn db_added_new_key() {
414 let cur = vec![make_db_sym(
415 1,
416 "new_fn",
417 SymbolKind::Function,
418 Some("fn new_fn()"),
419 None,
420 )];
421 let (changes, _) = compare_db_symbols(&[], &cur, "src/lib.rs");
422 assert_eq!(changes.len(), 1);
423 assert_eq!(changes[0].name, "new_fn");
424 assert_eq!(changes[0].change_type, ChangeType::Added);
425 }
426
427 #[test]
428 fn db_removed_key() {
429 let old = vec![make_db_sym(
430 1,
431 "old_fn",
432 SymbolKind::Function,
433 Some("fn old_fn()"),
434 None,
435 )];
436 let (changes, _) = compare_db_symbols(&old, &[], "src/lib.rs");
437 assert_eq!(changes.len(), 1);
438 assert_eq!(changes[0].name, "old_fn");
439 assert_eq!(changes[0].change_type, ChangeType::Removed);
440 }
441
442 #[test]
443 fn db_modified_signature() {
444 let old = vec![make_db_sym(
445 1,
446 "foo",
447 SymbolKind::Function,
448 Some("fn foo()"),
449 None,
450 )];
451 let cur = vec![make_db_sym(
452 1,
453 "foo",
454 SymbolKind::Function,
455 Some("fn foo(x: i32)"),
456 None,
457 )];
458 let (changes, mod_ids) = compare_db_symbols(&old, &cur, "src/lib.rs");
459 assert_eq!(changes.len(), 1);
460 assert_eq!(changes[0].change_type, ChangeType::Modified);
461 assert_eq!(mod_ids, vec![1]);
462 }
463
464 #[test]
465 fn db_unchanged() {
466 let old = vec![make_db_sym(
467 1,
468 "foo",
469 SymbolKind::Function,
470 Some("fn foo()"),
471 None,
472 )];
473 let cur = vec![make_db_sym(
474 1,
475 "foo",
476 SymbolKind::Function,
477 Some("fn foo()"),
478 None,
479 )];
480 let (changes, _) = compare_db_symbols(&old, &cur, "src/lib.rs");
481 assert!(changes.is_empty());
482 }
483
484 #[test]
485 fn db_budget_guard() {
486 let old: Vec<SymbolWithParent> = (0..6)
487 .map(|i| {
488 make_db_sym(
489 i,
490 &format!("fn_{i}"),
491 SymbolKind::Function,
492 Some(&format!("fn fn_{i}()")),
493 None,
494 )
495 })
496 .collect();
497 let (changes, mod_ids) = compare_db_symbols(&old, &[], "src/lib.rs");
498 assert_eq!(changes.len(), 6);
499 assert!(mod_ids.is_empty(), "budget guard should clear modified_ids");
500 }
501
502 #[test]
503 fn db_extra_current_beyond_paired() {
504 let old = vec![make_db_sym(
505 1,
506 "foo",
507 SymbolKind::Function,
508 Some("fn foo()"),
509 None,
510 )];
511 let cur = vec![
512 make_db_sym(1, "foo", SymbolKind::Function, Some("fn foo()"), None),
513 make_db_sym(2, "foo", SymbolKind::Function, Some("fn foo(x: i32)"), None),
514 ];
515 let (changes, _) = compare_db_symbols(&old, &cur, "src/lib.rs");
516 assert_eq!(changes.len(), 1);
517 assert_eq!(changes[0].change_type, ChangeType::Added);
518 }
519
520 #[test]
521 fn db_extra_old_beyond_paired() {
522 let old = vec![
523 make_db_sym(1, "foo", SymbolKind::Function, Some("fn foo()"), None),
524 make_db_sym(2, "foo", SymbolKind::Function, Some("fn foo(x: i32)"), None),
525 ];
526 let cur = vec![make_db_sym(
527 1,
528 "foo",
529 SymbolKind::Function,
530 Some("fn foo()"),
531 None,
532 )];
533 let (changes, _) = compare_db_symbols(&old, &cur, "src/lib.rs");
534 assert_eq!(changes.len(), 1);
535 assert_eq!(changes[0].change_type, ChangeType::Removed);
536 }
537
538 #[test]
541 fn change_type_equality() {
542 assert_eq!(ChangeType::Added, ChangeType::Added);
543 assert_eq!(ChangeType::Modified, ChangeType::Modified);
544 assert_eq!(ChangeType::Removed, ChangeType::Removed);
545 assert_ne!(ChangeType::Added, ChangeType::Removed);
546 }
547
548 #[test]
551 fn resolve_parent_name_with_parent() {
552 let symbols = vec![
553 make_extracted("MyClass", SymbolKind::Class, None, None),
554 make_extracted("method", SymbolKind::Method, None, Some(0)),
555 ];
556 assert_eq!(
557 resolve_parent_name(&symbols, &symbols[1]),
558 Some("MyClass".to_string())
559 );
560 }
561
562 #[test]
563 fn resolve_parent_name_without_parent() {
564 let symbols = vec![make_extracted("top_fn", SymbolKind::Function, None, None)];
565 assert_eq!(resolve_parent_name(&symbols, &symbols[0]), None);
566 }
567
568 #[test]
569 fn resolve_parent_name_out_of_bounds() {
570 let symbols = vec![make_extracted("orphan", SymbolKind::Method, None, Some(99))];
571 assert_eq!(resolve_parent_name(&symbols, &symbols[0]), None);
572 }
573}