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::lines::LineIndex;
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 old_span = tracing::info_span!(
70		"workspace.change_overlay.collect_base_references",
71		file.path = %base.file_path.display(),
72		graph.references = base.graph.ref_count(),
73	);
74	let mut old_facts = old_span.in_scope(|| collect_ref_facts(base, Some(ctx)));
75	let new_span = tracing::info_span!(
76		"workspace.change_overlay.collect_current_references",
77		file.path = %current.file_path.display(),
78		graph.references = current.graph.ref_count(),
79	);
80	let mut new_facts = new_span.in_scope(|| collect_ref_facts(current, None));
81	let cancel_span = tracing::info_span!(
82		"workspace.change_overlay.cancel_unchanged_references",
83		file.path = %current.file_path.display(),
84	);
85	cancel_span.in_scope(|| cancel_unchanged(&mut old_facts, &mut new_facts));
86	let retarget_span = tracing::info_span!(
87		"workspace.change_overlay.pair_retargeted_references",
88		file.path = %current.file_path.display(),
89	);
90	let mut changes =
91		retarget_span.in_scope(|| pair_retargets(&mut old_facts, &mut new_facts, current));
92	let materialize_span = tracing::info_span!(
93		"workspace.change_overlay.materialize_reference_changes",
94		file.path = %current.file_path.display(),
95	);
96	materialize_span.in_scope(|| {
97		changes.extend(old_facts.into_iter().flatten().map(|fact| RefChange {
98			kind: RefChangeKind::Removed,
99			file_path: base.file_path.to_path_buf(),
100			ref_kind: fact.ref_kind,
101			old_target: Some(fact.target),
102			new_target: None,
103			old_line_range: fact.line_range,
104			new_line_range: None,
105		}));
106		changes.extend(new_facts.into_iter().flatten().map(|fact| RefChange {
107			kind: RefChangeKind::Added,
108			file_path: current.file_path.to_path_buf(),
109			ref_kind: fact.ref_kind,
110			old_target: None,
111			new_target: Some(fact.target),
112			old_line_range: None,
113			new_line_range: fact.line_range,
114		}));
115	});
116	changes
117}
118
119fn collect_ref_facts(file: &FileSide<'_>, ctx: Option<&RenameContext>) -> Vec<Option<RefFact>> {
120	let lines = LineIndex::new(file.source);
121	file.graph
122		.refs()
123		.map(|record| Some(ref_fact(file, record, &lines, ctx)))
124		.collect()
125}
126
127fn ref_fact(
128	file: &FileSide<'_>,
129	record: &RefRecord,
130	lines: &LineIndex,
131	ctx: Option<&RenameContext>,
132) -> RefFact {
133	let source = file.graph.def_at(record.source).moniker.clone();
134	let raw_key = ref_key(record, &source, &record.target);
135	let mapped_key = ctx.and_then(|ctx| {
136		let mapped_source = ctx.apply(&source);
137		let mapped_target = ctx.apply(&record.target);
138		if mapped_source.is_none() && mapped_target.is_none() {
139			return None;
140		}
141		Some(ref_key(
142			record,
143			mapped_source.as_ref().unwrap_or(&source),
144			mapped_target.as_ref().unwrap_or(&record.target),
145		))
146	});
147	RefFact {
148		raw_key,
149		mapped_key,
150		ref_kind: ref_kind(record),
151		import: record.binding.as_ref() == BIND_IMPORT,
152		target: record.target.clone(),
153		line_range: record
154			.position
155			.map(|(start, end)| lines.line_range(start, end)),
156	}
157}
158
159fn ref_key(record: &RefRecord, source: &Moniker, target: &Moniker) -> RefKey {
160	(
161		source.as_encoded().to_vec(),
162		target.as_encoded().to_vec(),
163		record.kind.to_vec(),
164		record.call_arity,
165		record.alias.to_vec(),
166		record.binding.to_vec(),
167	)
168}
169
170fn cancel_unchanged(old_facts: &mut [Option<RefFact>], new_facts: &mut [Option<RefFact>]) {
171	let mut by_key: FxHashMap<RefKey, Vec<usize>> = FxHashMap::default();
172	for (idx, fact) in new_facts.iter().enumerate() {
173		if let Some(fact) = fact {
174			by_key.entry(fact.raw_key.clone()).or_default().push(idx);
175		}
176	}
177	for old_slot in old_facts.iter_mut() {
178		let Some(fact) = old_slot else { continue };
179		let Some(matches) = by_key.get_mut(&fact.raw_key) else {
180			continue;
181		};
182		let Some(new_idx) = matches.pop() else {
183			continue;
184		};
185		new_facts[new_idx] = None;
186		*old_slot = None;
187	}
188}
189
190fn pair_retargets(
191	old_facts: &mut [Option<RefFact>],
192	new_facts: &mut [Option<RefFact>],
193	current: &FileSide<'_>,
194) -> Vec<RefChange> {
195	let mut by_key: FxHashMap<RefKey, Vec<usize>> = FxHashMap::default();
196	for (idx, fact) in new_facts.iter().enumerate() {
197		if let Some(fact) = fact {
198			by_key.entry(fact.raw_key.clone()).or_default().push(idx);
199		}
200	}
201	let mut changes = Vec::new();
202	for old_slot in old_facts.iter_mut() {
203		let Some(fact) = old_slot else { continue };
204		let Some(mapped_key) = fact.mapped_key.as_ref() else {
205			continue;
206		};
207		let Some(new_idx) = by_key.get_mut(mapped_key).and_then(Vec::pop) else {
208			continue;
209		};
210		let old = old_slot.take().expect("checked above");
211		let new = new_facts[new_idx].take().expect("indexed above");
212		let kind = if old.import {
213			RefChangeKind::ImportRetargeted
214		} else {
215			RefChangeKind::CallSiteRetargeted
216		};
217		changes.push(RefChange {
218			kind,
219			file_path: current.file_path.to_path_buf(),
220			ref_kind: new.ref_kind,
221			old_target: Some(old.target),
222			new_target: Some(new.target),
223			old_line_range: old.line_range,
224			new_line_range: new.line_range,
225		});
226	}
227	changes
228}
229
230pub struct CoverageInputs<'a> {
231	pub old_hunks: &'a [(u32, u32)],
232	pub new_hunks: &'a [(u32, u32)],
233	pub old_explained: &'a [(u32, u32)],
234	pub new_explained: &'a [(u32, u32)],
235}
236
237pub fn hunk_coverage(inputs: CoverageInputs<'_>) -> HunkCoverage {
238	HunkCoverage {
239		old_residual: residual_spans(inputs.old_hunks, inputs.old_explained),
240		new_residual: residual_spans(inputs.new_hunks, inputs.new_explained),
241	}
242}
243
244fn residual_spans(hunks: &[(u32, u32)], explained: &[(u32, u32)]) -> Vec<(u32, u32)> {
245	let covered = merged_spans(explained);
246	let mut out = Vec::new();
247	for &(start, end) in hunks {
248		let mut cursor = start;
249		for &(covered_start, covered_end) in &covered {
250			if covered_end < cursor || covered_start > end {
251				continue;
252			}
253			if covered_start > cursor {
254				out.push((cursor, covered_start - 1));
255			}
256			cursor = cursor.max(covered_end.saturating_add(1));
257			if cursor > end {
258				break;
259			}
260		}
261		if cursor <= end {
262			out.push((cursor, end));
263		}
264	}
265	out
266}
267
268fn merged_spans(spans: &[(u32, u32)]) -> Vec<(u32, u32)> {
269	let mut sorted = spans.to_vec();
270	sorted.sort_unstable();
271	let mut merged: Vec<(u32, u32)> = Vec::new();
272	for (start, end) in sorted {
273		match merged.last_mut() {
274			Some(last) if start <= last.1.saturating_add(1) => last.1 = last.1.max(end),
275			_ => merged.push((start, end)),
276		}
277	}
278	merged
279}
280
281#[cfg(test)]
282mod tests {
283	use super::super::pairing::{PairInputs, finish_files, pair_file};
284	use super::*;
285	use crate::environment;
286	use code_moniker_core::core::moniker::MonikerBuilder;
287	use code_moniker_core::lang::Lang;
288	use std::path::Path;
289
290	struct Extraction {
291		graph: code_moniker_core::core::code_graph::CodeGraph,
292		source: String,
293		rel: String,
294	}
295
296	fn extract(source: &str, rel: &str) -> Extraction {
297		Extraction {
298			graph: environment::extract_source(Lang::Rs, source, Path::new(rel)),
299			source: source.to_string(),
300			rel: rel.to_string(),
301		}
302	}
303
304	fn file_side(extraction: &Extraction) -> FileSide<'_> {
305		FileSide {
306			lang: Lang::Rs,
307			graph: &extraction.graph,
308			source: &extraction.source,
309			file_path: Path::new(&extraction.rel),
310		}
311	}
312
313	fn moniker(segments: &[(&[u8], &[u8])]) -> Moniker {
314		let mut builder = MonikerBuilder::new();
315		builder.project(b"app");
316		for (kind, name) in segments {
317			builder.segment(kind, name);
318		}
319		builder.build()
320	}
321
322	#[test]
323	fn rename_context_uses_the_longest_indexed_ancestor() {
324		let old_module = moniker(&[(b"module", b"old")]);
325		let new_module = moniker(&[(b"module", b"new")]);
326		let old_function = moniker(&[(b"module", b"old"), (b"fn", b"work()")]);
327		let new_function = moniker(&[(b"module", b"new"), (b"fn", b"run()")]);
328		let target = moniker(&[
329			(b"module", b"old"),
330			(b"fn", b"work()"),
331			(b"local", b"value"),
332		]);
333		let expected = moniker(&[(b"module", b"new"), (b"fn", b"run()"), (b"local", b"value")]);
334		let mut context = RenameContext::from_changes(&[]);
335		context.push_pair(old_module, new_module);
336		context.push_pair(old_function, new_function);
337
338		assert_eq!(context.apply(&target), Some(expected));
339	}
340
341	#[test]
342	fn call_sites_retarget_after_a_rename() {
343		let base = extract(
344			"fn helper(x: u32) -> u32 { x }\nfn caller() { helper(1); helper(2); }\n",
345			"src/lib.rs",
346		);
347		let current = extract(
348			"fn assist(x: u32) -> u32 { x }\nfn caller() { assist(1); assist(2); }\n",
349			"src/lib.rs",
350		);
351		let symbol_changes = finish_files(vec![pair_file(PairInputs {
352			base: file_side(&base),
353			current: file_side(&current),
354			file_moved: false,
355		})]);
356		let ctx = RenameContext::from_changes(&symbol_changes);
357
358		let ref_changes = pair_refs(&file_side(&base), &file_side(&current), &ctx);
359
360		let retargeted_calls: Vec<_> = ref_changes
361			.iter()
362			.filter(|change| {
363				change.kind == RefChangeKind::CallSiteRetargeted && change.ref_kind == "calls"
364			})
365			.collect();
366		assert_eq!(retargeted_calls.len(), 2, "{ref_changes:?}");
367		assert!(
368			ref_changes.iter().all(|change| change.kind.is_retarget()),
369			"no stray added/removed refs: {ref_changes:?}"
370		);
371	}
372
373	#[test]
374	fn imports_retarget_through_a_module_prefix_pair() {
375		let base = extract(
376			"mod helpers;\nuse crate::helpers::assist;\n\nfn caller() { assist(); }\n",
377			"src/lib.rs",
378		);
379		let current = extract(
380			"mod support;\nuse crate::support::assist;\n\nfn caller() { assist(); }\n",
381			"src/lib.rs",
382		);
383		let old_module = extract("pub fn assist() {}\n", "src/helpers.rs");
384		let new_module = extract("pub fn assist() {}\n", "src/support.rs");
385		let mut ctx = RenameContext::from_changes(&[]);
386		ctx.push_pair(
387			old_module.graph.root().clone(),
388			new_module.graph.root().clone(),
389		);
390
391		let ref_changes = pair_refs(&file_side(&base), &file_side(&current), &ctx);
392
393		assert!(
394			ref_changes
395				.iter()
396				.any(|change| change.kind == RefChangeKind::ImportRetargeted
397					&& change.ref_kind == "imports_symbol"),
398			"symbol import must retarget through the module prefix: {ref_changes:?}"
399		);
400		assert!(
401			ref_changes.iter().all(|change| change.kind.is_retarget()),
402			"{ref_changes:?}"
403		);
404	}
405
406	#[test]
407	fn unrelated_ref_edits_stay_added_and_removed() {
408		let base = extract("fn caller() { alpha(); }\n", "src/lib.rs");
409		let current = extract("fn caller() { beta(); }\n", "src/lib.rs");
410		let ctx = RenameContext::from_changes(&[]);
411
412		let ref_changes = pair_refs(&file_side(&base), &file_side(&current), &ctx);
413
414		let labels: Vec<_> = ref_changes.iter().map(|change| change.kind).collect();
415		assert!(labels.contains(&RefChangeKind::Added), "{ref_changes:?}");
416		assert!(labels.contains(&RefChangeKind::Removed), "{ref_changes:?}");
417	}
418
419	#[test]
420	fn coverage_subtracts_explained_spans() {
421		let coverage = hunk_coverage(CoverageInputs {
422			old_hunks: &[],
423			new_hunks: &[(10, 20), (30, 31)],
424			old_explained: &[],
425			new_explained: &[(9, 15), (18, 20)],
426		});
427
428		assert_eq!(coverage.new_residual, vec![(16, 17), (30, 31)]);
429		assert!(!coverage.explained());
430	}
431
432	#[test]
433	fn coverage_is_explained_when_all_hunks_are_covered() {
434		let coverage = hunk_coverage(CoverageInputs {
435			old_hunks: &[(5, 6)],
436			new_hunks: &[(10, 20)],
437			old_explained: &[(1, 8)],
438			new_explained: &[(10, 14), (15, 20)],
439		});
440
441		assert!(coverage.explained(), "{coverage:?}");
442	}
443}