Skip to main content

code_moniker_workspace/changes/semantic/
fingerprint.rs

1use code_moniker_core::core::moniker::Moniker;
2use rustc_hash::FxHasher;
3use std::hash::Hasher;
4
5#[derive(Clone, Debug, Eq, Hash, PartialEq)]
6pub struct TailSegment {
7	pub kind: Vec<u8>,
8	pub name: Vec<u8>,
9}
10
11#[derive(Clone, Debug, Eq, Hash, PartialEq)]
12pub struct IdentityTail {
13	segments: Vec<TailSegment>,
14}
15
16impl IdentityTail {
17	pub fn parent(&self) -> IdentityTail {
18		let mut segments = self.segments.clone();
19		segments.pop();
20		IdentityTail { segments }
21	}
22
23	pub fn last(&self) -> Option<&TailSegment> {
24		self.segments.last()
25	}
26
27	pub fn segments(&self) -> &[TailSegment] {
28		&self.segments
29	}
30
31	pub fn starts_with(&self, prefix: &IdentityTail) -> bool {
32		self.segments.starts_with(&prefix.segments)
33	}
34
35	pub fn rewrite_prefix(&self, from: &IdentityTail, to: &IdentityTail) -> Option<IdentityTail> {
36		let rest = self.segments.strip_prefix(from.segments.as_slice())?;
37		let mut segments = to.segments.clone();
38		segments.extend_from_slice(rest);
39		Some(IdentityTail { segments })
40	}
41}
42
43pub fn identity_tail(moniker: &Moniker, root: &Moniker) -> Option<IdentityTail> {
44	let root_view = root.as_view();
45	let def_view = moniker.as_view();
46	if !root_view.is_ancestor_of(&def_view) {
47		return None;
48	}
49	let segments = def_view
50		.segments()
51		.skip(root_view.segment_count() as usize)
52		.map(|segment| TailSegment {
53			kind: segment.kind.to_vec(),
54			name: segment.name.to_vec(),
55		})
56		.collect();
57	Some(IdentityTail { segments })
58}
59
60pub fn split_callable_name(name: &[u8]) -> (&[u8], Option<&[u8]>) {
61	match name.iter().position(|byte| *byte == b'(') {
62		Some(open) => (&name[..open], Some(&name[open..])),
63		None => (name, None),
64	}
65}
66
67#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
68pub struct DefFingerprints {
69	pub text: u64,
70	pub body: u64,
71	pub full: u64,
72}
73
74pub struct FingerprintScope<'a> {
75	pub source: &'a str,
76	pub span: (u32, u32),
77	pub name: &'a [u8],
78	pub nested_spans: &'a [(u32, u32)],
79}
80
81pub fn def_fingerprints(scope: FingerprintScope<'_>) -> DefFingerprints {
82	let bytes = span_bytes(scope.source, scope.span);
83	let holes = relative_holes(scope.span, scope.nested_spans, bytes.len());
84	let text = masked_hash(bytes, scope.name, &holes);
85	let body = match find_identifier(bytes, scope.name) {
86		Some(at) if at > 0 => {
87			let shifted: Vec<(usize, usize)> = holes
88				.iter()
89				.filter(|(_, end)| *end > at)
90				.map(|(start, end)| (start.saturating_sub(at), end - at))
91				.collect();
92			masked_hash(&bytes[at..], scope.name, &shifted)
93		}
94		_ => text,
95	};
96	let full = if holes.is_empty() {
97		text
98	} else {
99		masked_hash(bytes, scope.name, &[])
100	};
101	DefFingerprints { text, body, full }
102}
103
104fn relative_holes(span: (u32, u32), nested: &[(u32, u32)], len: usize) -> Vec<(usize, usize)> {
105	let mut holes: Vec<(usize, usize)> = nested
106		.iter()
107		.filter(|inner| **inner != span && inner.0 >= span.0 && inner.1 <= span.1)
108		.map(|inner| {
109			let start = ((inner.0 - span.0) as usize).min(len);
110			let end = ((inner.1 - span.0) as usize).min(len);
111			(start, end)
112		})
113		.collect();
114	holes.sort_unstable();
115	holes
116}
117
118fn span_bytes(source: &str, span: (u32, u32)) -> &[u8] {
119	let bytes = source.as_bytes();
120	let start = (span.0 as usize).min(bytes.len());
121	let end = (span.1 as usize).clamp(start, bytes.len());
122	&bytes[start..end]
123}
124
125fn masked_hash(bytes: &[u8], mask: &[u8], holes: &[(usize, usize)]) -> u64 {
126	let mut hasher = FxHasher::default();
127	let mut pending_space = false;
128	let mut emitted = false;
129	let mut cursor = 0;
130	let mut next_hole = 0;
131	while cursor < bytes.len() {
132		while next_hole < holes.len() && holes[next_hole].1 <= cursor {
133			next_hole += 1;
134		}
135		if next_hole < holes.len() && holes[next_hole].0 <= cursor {
136			cursor = holes[next_hole].1;
137			next_hole += 1;
138			pending_space = emitted;
139			continue;
140		}
141		if matches_identifier_at(bytes, cursor, mask) {
142			if pending_space {
143				hasher.write_u8(b' ');
144				pending_space = false;
145			}
146			hasher.write_u8(0);
147			emitted = true;
148			cursor += mask.len();
149			continue;
150		}
151		let byte = bytes[cursor];
152		cursor += 1;
153		if matches!(byte, b' ' | b'\t' | b'\r' | b'\n') {
154			pending_space = emitted;
155			continue;
156		}
157		if pending_space {
158			hasher.write_u8(b' ');
159			pending_space = false;
160		}
161		hasher.write_u8(byte);
162		emitted = true;
163	}
164	hasher.finish()
165}
166
167fn find_identifier(bytes: &[u8], name: &[u8]) -> Option<usize> {
168	if name.is_empty() {
169		return None;
170	}
171	(0..bytes.len().saturating_sub(name.len() - 1))
172		.find(|&at| matches_identifier_at(bytes, at, name))
173}
174
175fn matches_identifier_at(bytes: &[u8], at: usize, name: &[u8]) -> bool {
176	if name.is_empty() || !bytes[at..].starts_with(name) {
177		return false;
178	}
179	let before = at.checked_sub(1).map(|i| bytes[i]);
180	let after = bytes.get(at + name.len()).copied();
181	!before.is_some_and(is_identifier_byte) && !after.is_some_and(is_identifier_byte)
182}
183
184fn is_identifier_byte(byte: u8) -> bool {
185	byte.is_ascii_alphanumeric() || byte == b'_'
186}
187
188#[cfg(test)]
189mod tests {
190	use super::*;
191	use crate::environment;
192	use code_moniker_core::lang::Lang;
193	use std::path::Path;
194
195	fn prints(source: &str, name: &[u8]) -> DefFingerprints {
196		def_fingerprints(FingerprintScope {
197			source,
198			span: (0, source.len() as u32),
199			name,
200			nested_spans: &[],
201		})
202	}
203
204	#[test]
205	fn fingerprint_ignores_whitespace_layout() {
206		assert_eq!(
207			prints("fn f() { let x = 1; }", b"f").text,
208			prints("fn f() {\n\tlet x = 1;\n}", b"f").text
209		);
210		assert_eq!(
211			prints("  fn f() {}", b"f").text,
212			prints("fn f() {}\n", b"f").text
213		);
214	}
215
216	#[test]
217	fn fingerprint_distinguishes_token_changes() {
218		assert_ne!(
219			prints("fn f() { let x = 1; }", b"f").text,
220			prints("fn f() { let x = 2; }", b"f").text
221		);
222		assert_ne!(
223			prints("let ab = 1;", b"").text,
224			prints("let a b = 1;", b"").text
225		);
226	}
227
228	#[test]
229	fn fingerprint_masks_the_own_name_including_recursion() {
230		assert_eq!(
231			prints("fn old_name(n: u32) { old_name(n - 1); }", b"old_name").text,
232			prints("fn fresh(n: u32) { fresh(n - 1); }", b"fresh").text
233		);
234		assert_ne!(
235			prints("fn old_name(n: u32) { other(n); }", b"old_name").text,
236			prints("fn fresh(n: u32) { fresh(n); }", b"fresh").text
237		);
238	}
239
240	#[test]
241	fn fingerprint_does_not_mask_identifier_substrings() {
242		assert_ne!(prints("fetch();", b"f").text, prints("getch();", b"g").text);
243		assert_eq!(
244			prints("fn f() { fetch(); }", b"f").text,
245			prints("fn g() { fetch(); }", b"g").text
246		);
247	}
248
249	#[test]
250	fn body_fingerprint_starts_at_the_name() {
251		let public = prints("pub fn f() { work(); }", b"f");
252		let private = prints("fn f() { work(); }", b"f");
253		assert_eq!(public.body, private.body);
254		assert_ne!(public.text, private.text);
255	}
256
257	#[test]
258	fn identity_tail_is_stable_across_anchor_paths() {
259		let source =
260			"struct Holder;\nimpl Holder {\n\tfn touch(&self) {}\n}\nfn free_fn(x: u32) {}\n";
261		let left = environment::extract_source(Lang::Rs, source, Path::new("src/original.rs"));
262		let right =
263			environment::extract_source(Lang::Rs, source, Path::new("moved/deep/renamed.rs"));
264		let left_tails: Vec<_> = left
265			.defs()
266			.map(|def| identity_tail(&def.moniker, left.root()).expect("tail under root"))
267			.collect();
268		let right_tails: Vec<_> = right
269			.defs()
270			.map(|def| identity_tail(&def.moniker, right.root()).expect("tail under root"))
271			.collect();
272
273		assert_eq!(left_tails, right_tails);
274		assert!(
275			left_tails.iter().any(|tail| tail
276				.last()
277				.is_some_and(|seg| seg.name.starts_with(b"free_fn"))),
278			"expected a free_fn tail: {left_tails:?}"
279		);
280	}
281
282	#[test]
283	fn identity_tail_rejects_foreign_roots() {
284		let source = "fn lone() {}\n";
285		let graph = environment::extract_source(Lang::Rs, source, Path::new("src/a.rs"));
286		let other = environment::extract_source(Lang::Rs, source, Path::new("src/b.rs"));
287		let def = graph.defs().next().expect("def");
288
289		assert!(identity_tail(&def.moniker, other.root()).is_none());
290	}
291
292	#[test]
293	fn tail_parent_drops_the_final_segment() {
294		let source = "struct Holder;\nimpl Holder {\n\tfn touch(&self) {}\n}\n";
295		let graph = environment::extract_source(Lang::Rs, source, Path::new("src/a.rs"));
296		let method = graph
297			.defs()
298			.find(|def| crate::code::def_kind(def) == "method")
299			.expect("method def");
300		let tail = identity_tail(&method.moniker, graph.root()).expect("tail");
301		let parent = tail.parent();
302
303		assert_eq!(tail.segments().len(), parent.segments().len() + 1);
304		assert!(
305			parent
306				.last()
307				.is_some_and(|seg| seg.name.as_slice() == b"Holder"),
308			"parent tail should end at the impl target: {parent:?}"
309		);
310	}
311
312	#[test]
313	fn tail_rewrite_prefix_moves_children_under_the_new_container() {
314		let source = "struct Holder;\nimpl Holder {\n\tfn touch(&self) {}\n}\nstruct Keeper;\nimpl Keeper {\n\tfn touch(&self) {}\n}\n";
315		let graph = environment::extract_source(Lang::Rs, source, Path::new("src/a.rs"));
316		let tails: Vec<_> = graph
317			.defs()
318			.filter(|def| crate::code::def_kind(def) == "method")
319			.map(|def| identity_tail(&def.moniker, graph.root()).expect("tail"))
320			.collect();
321		let [holder_touch, keeper_touch] = tails.as_slice() else {
322			panic!("expected two methods: {tails:?}");
323		};
324
325		let rewritten = holder_touch
326			.rewrite_prefix(&holder_touch.parent(), &keeper_touch.parent())
327			.expect("prefix applies");
328
329		assert_eq!(&rewritten, keeper_touch);
330		assert!(
331			keeper_touch
332				.parent()
333				.rewrite_prefix(holder_touch, keeper_touch)
334				.is_none(),
335			"non-prefix rewrite must not apply"
336		);
337	}
338
339	#[test]
340	fn split_callable_name_separates_params() {
341		assert_eq!(
342			split_callable_name(b"findById(id:int,label:String)"),
343			(
344				b"findById".as_slice(),
345				Some(b"(id:int,label:String)".as_slice())
346			)
347		);
348		assert_eq!(split_callable_name(b"Holder"), (b"Holder".as_slice(), None));
349	}
350}