Skip to main content

code_moniker_workspace/changes/semantic/
refpairs.rs

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(&current),
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(&current), &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(&current), &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(&current), &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}