1use code_moniker_core::core::code_graph::RefRecord;
2use code_moniker_core::core::kinds::BIND_IMPORT;
3use code_moniker_core::core::moniker::Moniker;
4use rustc_hash::FxHashMap;
5
6use crate::code::ref_kind;
7use crate::environment;
8
9use super::model::{HunkCoverage, RefChange, RefChangeKind, SymbolChange};
10use super::pairing::FileSide;
11
12pub struct RenameContext {
13 pairs: Vec<(Moniker, Moniker)>,
14}
15
16impl RenameContext {
17 pub fn from_changes(changes: &[SymbolChange]) -> Self {
18 let pairs = changes
19 .iter()
20 .filter_map(|change| {
21 let old = change.old.as_ref()?.moniker.clone();
22 let new = change.new.as_ref()?.moniker.clone();
23 (old != new).then_some((old, new))
24 })
25 .collect();
26 Self { pairs }
27 }
28
29 pub fn push_pair(&mut self, old: Moniker, new: Moniker) {
30 if old != new {
31 self.pairs.push((old, new));
32 }
33 }
34
35 fn apply(&self, target: &Moniker) -> Option<Moniker> {
36 let view = target.as_view();
37 let mut best: Option<&(Moniker, Moniker)> = None;
38 for pair in &self.pairs {
39 if !pair.0.as_view().is_ancestor_of(&view) {
40 continue;
41 }
42 if best.is_none_or(|kept| pair.0.as_encoded().len() > kept.0.as_encoded().len()) {
43 best = Some(pair);
44 }
45 }
46 let (from, to) = best?;
47 let mut bytes = to.as_encoded().to_vec();
48 bytes.extend_from_slice(&target.as_encoded()[from.as_encoded().len()..]);
49 Moniker::from_encoded(bytes).ok()
50 }
51}
52
53type RefKey = (Vec<u8>, Vec<u8>, Vec<u8>, Option<usize>, Vec<u8>, Vec<u8>);
54
55struct RefFact {
56 raw_key: RefKey,
57 mapped_key: Option<RefKey>,
58 ref_kind: String,
59 import: bool,
60 target: Moniker,
61 line_range: Option<(u32, u32)>,
62}
63
64pub fn pair_refs(
65 base: &FileSide<'_>,
66 current: &FileSide<'_>,
67 ctx: &RenameContext,
68) -> Vec<RefChange> {
69 let mut old_facts = collect_ref_facts(base, Some(ctx));
70 let mut new_facts = collect_ref_facts(current, None);
71 cancel_unchanged(&mut old_facts, &mut new_facts);
72 let mut changes = pair_retargets(&mut old_facts, &mut new_facts, current);
73 changes.extend(old_facts.into_iter().flatten().map(|fact| RefChange {
74 kind: RefChangeKind::Removed,
75 file_path: base.file_path.to_path_buf(),
76 ref_kind: fact.ref_kind,
77 old_target: Some(fact.target),
78 new_target: None,
79 old_line_range: fact.line_range,
80 new_line_range: None,
81 }));
82 changes.extend(new_facts.into_iter().flatten().map(|fact| RefChange {
83 kind: RefChangeKind::Added,
84 file_path: current.file_path.to_path_buf(),
85 ref_kind: fact.ref_kind,
86 old_target: None,
87 new_target: Some(fact.target),
88 old_line_range: None,
89 new_line_range: fact.line_range,
90 }));
91 changes
92}
93
94fn collect_ref_facts(file: &FileSide<'_>, ctx: Option<&RenameContext>) -> Vec<Option<RefFact>> {
95 file.graph
96 .refs()
97 .map(|record| Some(ref_fact(file, record, ctx)))
98 .collect()
99}
100
101fn ref_fact(file: &FileSide<'_>, record: &RefRecord, ctx: Option<&RenameContext>) -> RefFact {
102 let source = file.graph.def_at(record.source).moniker.clone();
103 let raw_key = ref_key(record, &source, &record.target);
104 let mapped_key = ctx.and_then(|ctx| {
105 let mapped_source = ctx.apply(&source);
106 let mapped_target = ctx.apply(&record.target);
107 if mapped_source.is_none() && mapped_target.is_none() {
108 return None;
109 }
110 Some(ref_key(
111 record,
112 mapped_source.as_ref().unwrap_or(&source),
113 mapped_target.as_ref().unwrap_or(&record.target),
114 ))
115 });
116 RefFact {
117 raw_key,
118 mapped_key,
119 ref_kind: ref_kind(record),
120 import: record.binding.as_ref() == BIND_IMPORT,
121 target: record.target.clone(),
122 line_range: record
123 .position
124 .map(|(start, end)| environment::line_range(file.source, start, end)),
125 }
126}
127
128fn ref_key(record: &RefRecord, source: &Moniker, target: &Moniker) -> RefKey {
129 (
130 source.as_encoded().to_vec(),
131 target.as_encoded().to_vec(),
132 record.kind.to_vec(),
133 record.call_arity,
134 record.alias.to_vec(),
135 record.binding.to_vec(),
136 )
137}
138
139fn cancel_unchanged(old_facts: &mut [Option<RefFact>], new_facts: &mut [Option<RefFact>]) {
140 let mut by_key: FxHashMap<RefKey, Vec<usize>> = FxHashMap::default();
141 for (idx, fact) in new_facts.iter().enumerate() {
142 if let Some(fact) = fact {
143 by_key.entry(fact.raw_key.clone()).or_default().push(idx);
144 }
145 }
146 for old_slot in old_facts.iter_mut() {
147 let Some(fact) = old_slot else { continue };
148 let Some(matches) = by_key.get_mut(&fact.raw_key) else {
149 continue;
150 };
151 let Some(new_idx) = matches.pop() else {
152 continue;
153 };
154 new_facts[new_idx] = None;
155 *old_slot = None;
156 }
157}
158
159fn pair_retargets(
160 old_facts: &mut [Option<RefFact>],
161 new_facts: &mut [Option<RefFact>],
162 current: &FileSide<'_>,
163) -> Vec<RefChange> {
164 let mut by_key: FxHashMap<RefKey, Vec<usize>> = FxHashMap::default();
165 for (idx, fact) in new_facts.iter().enumerate() {
166 if let Some(fact) = fact {
167 by_key.entry(fact.raw_key.clone()).or_default().push(idx);
168 }
169 }
170 let mut changes = Vec::new();
171 for old_slot in old_facts.iter_mut() {
172 let Some(fact) = old_slot else { continue };
173 let Some(mapped_key) = fact.mapped_key.as_ref() else {
174 continue;
175 };
176 let Some(new_idx) = by_key.get_mut(mapped_key).and_then(Vec::pop) else {
177 continue;
178 };
179 let old = old_slot.take().expect("checked above");
180 let new = new_facts[new_idx].take().expect("indexed above");
181 let kind = if old.import {
182 RefChangeKind::ImportRetargeted
183 } else {
184 RefChangeKind::CallSiteRetargeted
185 };
186 changes.push(RefChange {
187 kind,
188 file_path: current.file_path.to_path_buf(),
189 ref_kind: new.ref_kind,
190 old_target: Some(old.target),
191 new_target: Some(new.target),
192 old_line_range: old.line_range,
193 new_line_range: new.line_range,
194 });
195 }
196 changes
197}
198
199pub struct CoverageInputs<'a> {
200 pub old_hunks: &'a [(u32, u32)],
201 pub new_hunks: &'a [(u32, u32)],
202 pub old_explained: &'a [(u32, u32)],
203 pub new_explained: &'a [(u32, u32)],
204}
205
206pub fn hunk_coverage(inputs: CoverageInputs<'_>) -> HunkCoverage {
207 HunkCoverage {
208 old_residual: residual_spans(inputs.old_hunks, inputs.old_explained),
209 new_residual: residual_spans(inputs.new_hunks, inputs.new_explained),
210 }
211}
212
213fn residual_spans(hunks: &[(u32, u32)], explained: &[(u32, u32)]) -> Vec<(u32, u32)> {
214 let covered = merged_spans(explained);
215 let mut out = Vec::new();
216 for &(start, end) in hunks {
217 let mut cursor = start;
218 for &(covered_start, covered_end) in &covered {
219 if covered_end < cursor || covered_start > end {
220 continue;
221 }
222 if covered_start > cursor {
223 out.push((cursor, covered_start - 1));
224 }
225 cursor = cursor.max(covered_end.saturating_add(1));
226 if cursor > end {
227 break;
228 }
229 }
230 if cursor <= end {
231 out.push((cursor, end));
232 }
233 }
234 out
235}
236
237fn merged_spans(spans: &[(u32, u32)]) -> Vec<(u32, u32)> {
238 let mut sorted = spans.to_vec();
239 sorted.sort_unstable();
240 let mut merged: Vec<(u32, u32)> = Vec::new();
241 for (start, end) in sorted {
242 match merged.last_mut() {
243 Some(last) if start <= last.1.saturating_add(1) => last.1 = last.1.max(end),
244 _ => merged.push((start, end)),
245 }
246 }
247 merged
248}
249
250#[cfg(test)]
251mod tests {
252 use super::super::pairing::{PairInputs, finish_files, pair_file};
253 use super::*;
254 use code_moniker_core::lang::Lang;
255 use std::path::Path;
256
257 struct Extraction {
258 graph: code_moniker_core::core::code_graph::CodeGraph,
259 source: String,
260 rel: String,
261 }
262
263 fn extract(source: &str, rel: &str) -> Extraction {
264 Extraction {
265 graph: environment::extract_source(Lang::Rs, source, Path::new(rel)),
266 source: source.to_string(),
267 rel: rel.to_string(),
268 }
269 }
270
271 fn file_side(extraction: &Extraction) -> FileSide<'_> {
272 FileSide {
273 lang: Lang::Rs,
274 graph: &extraction.graph,
275 source: &extraction.source,
276 file_path: Path::new(&extraction.rel),
277 }
278 }
279
280 #[test]
281 fn call_sites_retarget_after_a_rename() {
282 let base = extract(
283 "fn helper(x: u32) -> u32 { x }\nfn caller() { helper(1); helper(2); }\n",
284 "src/lib.rs",
285 );
286 let current = extract(
287 "fn assist(x: u32) -> u32 { x }\nfn caller() { assist(1); assist(2); }\n",
288 "src/lib.rs",
289 );
290 let symbol_changes = finish_files(vec![pair_file(PairInputs {
291 base: file_side(&base),
292 current: file_side(¤t),
293 file_moved: false,
294 })]);
295 let ctx = RenameContext::from_changes(&symbol_changes);
296
297 let ref_changes = pair_refs(&file_side(&base), &file_side(¤t), &ctx);
298
299 let retargeted_calls: Vec<_> = ref_changes
300 .iter()
301 .filter(|change| {
302 change.kind == RefChangeKind::CallSiteRetargeted && change.ref_kind == "calls"
303 })
304 .collect();
305 assert_eq!(retargeted_calls.len(), 2, "{ref_changes:?}");
306 assert!(
307 ref_changes.iter().all(|change| change.kind.is_retarget()),
308 "no stray added/removed refs: {ref_changes:?}"
309 );
310 }
311
312 #[test]
313 fn imports_retarget_through_a_module_prefix_pair() {
314 let base = extract(
315 "mod helpers;\nuse crate::helpers::assist;\n\nfn caller() { assist(); }\n",
316 "src/lib.rs",
317 );
318 let current = extract(
319 "mod support;\nuse crate::support::assist;\n\nfn caller() { assist(); }\n",
320 "src/lib.rs",
321 );
322 let old_module = extract("pub fn assist() {}\n", "src/helpers.rs");
323 let new_module = extract("pub fn assist() {}\n", "src/support.rs");
324 let mut ctx = RenameContext::from_changes(&[]);
325 ctx.push_pair(
326 old_module.graph.root().clone(),
327 new_module.graph.root().clone(),
328 );
329
330 let ref_changes = pair_refs(&file_side(&base), &file_side(¤t), &ctx);
331
332 assert!(
333 ref_changes
334 .iter()
335 .any(|change| change.kind == RefChangeKind::ImportRetargeted
336 && change.ref_kind == "imports_symbol"),
337 "symbol import must retarget through the module prefix: {ref_changes:?}"
338 );
339 assert!(
340 ref_changes.iter().all(|change| change.kind.is_retarget()),
341 "{ref_changes:?}"
342 );
343 }
344
345 #[test]
346 fn unrelated_ref_edits_stay_added_and_removed() {
347 let base = extract("fn caller() { alpha(); }\n", "src/lib.rs");
348 let current = extract("fn caller() { beta(); }\n", "src/lib.rs");
349 let ctx = RenameContext::from_changes(&[]);
350
351 let ref_changes = pair_refs(&file_side(&base), &file_side(¤t), &ctx);
352
353 let labels: Vec<_> = ref_changes.iter().map(|change| change.kind).collect();
354 assert!(labels.contains(&RefChangeKind::Added), "{ref_changes:?}");
355 assert!(labels.contains(&RefChangeKind::Removed), "{ref_changes:?}");
356 }
357
358 #[test]
359 fn coverage_subtracts_explained_spans() {
360 let coverage = hunk_coverage(CoverageInputs {
361 old_hunks: &[],
362 new_hunks: &[(10, 20), (30, 31)],
363 old_explained: &[],
364 new_explained: &[(9, 15), (18, 20)],
365 });
366
367 assert_eq!(coverage.new_residual, vec![(16, 17), (30, 31)]);
368 assert!(!coverage.explained());
369 }
370
371 #[test]
372 fn coverage_is_explained_when_all_hunks_are_covered() {
373 let coverage = hunk_coverage(CoverageInputs {
374 old_hunks: &[(5, 6)],
375 new_hunks: &[(10, 20)],
376 old_explained: &[(1, 8)],
377 new_explained: &[(10, 14), (15, 20)],
378 });
379
380 assert!(coverage.explained(), "{coverage:?}");
381 }
382}