code_moniker_workspace/changes/semantic/
fingerprint.rs1use 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}