code-moniker-workspace 0.4.0

Workspace model, ports, snapshots, linkage, and change analysis for code-moniker.
Documentation
use code_moniker_core::core::moniker::Moniker;
use rustc_hash::FxHasher;
use std::hash::Hasher;

#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct TailSegment {
	pub kind: Vec<u8>,
	pub name: Vec<u8>,
}

#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct IdentityTail {
	segments: Vec<TailSegment>,
}

impl IdentityTail {
	pub fn parent(&self) -> IdentityTail {
		let mut segments = self.segments.clone();
		segments.pop();
		IdentityTail { segments }
	}

	pub fn last(&self) -> Option<&TailSegment> {
		self.segments.last()
	}

	pub fn segments(&self) -> &[TailSegment] {
		&self.segments
	}

	pub fn starts_with(&self, prefix: &IdentityTail) -> bool {
		self.segments.starts_with(&prefix.segments)
	}

	pub fn rewrite_prefix(&self, from: &IdentityTail, to: &IdentityTail) -> Option<IdentityTail> {
		let rest = self.segments.strip_prefix(from.segments.as_slice())?;
		let mut segments = to.segments.clone();
		segments.extend_from_slice(rest);
		Some(IdentityTail { segments })
	}
}

pub fn identity_tail(moniker: &Moniker, root: &Moniker) -> Option<IdentityTail> {
	let root_view = root.as_view();
	let def_view = moniker.as_view();
	if !root_view.is_ancestor_of(&def_view) {
		return None;
	}
	let segments = def_view
		.segments()
		.skip(root_view.segment_count() as usize)
		.map(|segment| TailSegment {
			kind: segment.kind.to_vec(),
			name: segment.name.to_vec(),
		})
		.collect();
	Some(IdentityTail { segments })
}

pub fn split_callable_name(name: &[u8]) -> (&[u8], Option<&[u8]>) {
	match name.iter().position(|byte| *byte == b'(') {
		Some(open) => (&name[..open], Some(&name[open..])),
		None => (name, None),
	}
}

#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct DefFingerprints {
	pub text: u64,
	pub body: u64,
	pub full: u64,
}

pub struct FingerprintScope<'a> {
	pub source: &'a str,
	pub span: (u32, u32),
	pub name: &'a [u8],
	pub nested_spans: &'a [(u32, u32)],
}

pub fn def_fingerprints(scope: FingerprintScope<'_>) -> DefFingerprints {
	let bytes = span_bytes(scope.source, scope.span);
	let holes = relative_holes(scope.span, scope.nested_spans, bytes.len());
	let text = masked_hash(bytes, scope.name, &holes);
	let body = match find_identifier(bytes, scope.name) {
		Some(at) if at > 0 => {
			let shifted: Vec<(usize, usize)> = holes
				.iter()
				.filter(|(_, end)| *end > at)
				.map(|(start, end)| (start.saturating_sub(at), end - at))
				.collect();
			masked_hash(&bytes[at..], scope.name, &shifted)
		}
		_ => text,
	};
	let full = if holes.is_empty() {
		text
	} else {
		masked_hash(bytes, scope.name, &[])
	};
	DefFingerprints { text, body, full }
}

fn relative_holes(span: (u32, u32), nested: &[(u32, u32)], len: usize) -> Vec<(usize, usize)> {
	let mut holes: Vec<(usize, usize)> = nested
		.iter()
		.filter(|inner| **inner != span && inner.0 >= span.0 && inner.1 <= span.1)
		.map(|inner| {
			let start = ((inner.0 - span.0) as usize).min(len);
			let end = ((inner.1 - span.0) as usize).min(len);
			(start, end)
		})
		.collect();
	holes.sort_unstable();
	holes
}

fn span_bytes(source: &str, span: (u32, u32)) -> &[u8] {
	let bytes = source.as_bytes();
	let start = (span.0 as usize).min(bytes.len());
	let end = (span.1 as usize).clamp(start, bytes.len());
	&bytes[start..end]
}

fn masked_hash(bytes: &[u8], mask: &[u8], holes: &[(usize, usize)]) -> u64 {
	let mut hasher = FxHasher::default();
	let mut pending_space = false;
	let mut emitted = false;
	let mut cursor = 0;
	let mut next_hole = 0;
	while cursor < bytes.len() {
		while next_hole < holes.len() && holes[next_hole].1 <= cursor {
			next_hole += 1;
		}
		if next_hole < holes.len() && holes[next_hole].0 <= cursor {
			cursor = holes[next_hole].1;
			next_hole += 1;
			pending_space = emitted;
			continue;
		}
		if matches_identifier_at(bytes, cursor, mask) {
			if pending_space {
				hasher.write_u8(b' ');
				pending_space = false;
			}
			hasher.write_u8(0);
			emitted = true;
			cursor += mask.len();
			continue;
		}
		let byte = bytes[cursor];
		cursor += 1;
		if matches!(byte, b' ' | b'\t' | b'\r' | b'\n') {
			pending_space = emitted;
			continue;
		}
		if pending_space {
			hasher.write_u8(b' ');
			pending_space = false;
		}
		hasher.write_u8(byte);
		emitted = true;
	}
	hasher.finish()
}

fn find_identifier(bytes: &[u8], name: &[u8]) -> Option<usize> {
	if name.is_empty() {
		return None;
	}
	(0..bytes.len().saturating_sub(name.len() - 1))
		.find(|&at| matches_identifier_at(bytes, at, name))
}

fn matches_identifier_at(bytes: &[u8], at: usize, name: &[u8]) -> bool {
	if name.is_empty() || !bytes[at..].starts_with(name) {
		return false;
	}
	let before = at.checked_sub(1).map(|i| bytes[i]);
	let after = bytes.get(at + name.len()).copied();
	!before.is_some_and(is_identifier_byte) && !after.is_some_and(is_identifier_byte)
}

fn is_identifier_byte(byte: u8) -> bool {
	byte.is_ascii_alphanumeric() || byte == b'_'
}

#[cfg(test)]
mod tests {
	use super::*;
	use crate::environment;
	use code_moniker_core::lang::Lang;
	use std::path::Path;

	fn prints(source: &str, name: &[u8]) -> DefFingerprints {
		def_fingerprints(FingerprintScope {
			source,
			span: (0, source.len() as u32),
			name,
			nested_spans: &[],
		})
	}

	#[test]
	fn fingerprint_ignores_whitespace_layout() {
		assert_eq!(
			prints("fn f() { let x = 1; }", b"f").text,
			prints("fn f() {\n\tlet x = 1;\n}", b"f").text
		);
		assert_eq!(
			prints("  fn f() {}", b"f").text,
			prints("fn f() {}\n", b"f").text
		);
	}

	#[test]
	fn fingerprint_distinguishes_token_changes() {
		assert_ne!(
			prints("fn f() { let x = 1; }", b"f").text,
			prints("fn f() { let x = 2; }", b"f").text
		);
		assert_ne!(
			prints("let ab = 1;", b"").text,
			prints("let a b = 1;", b"").text
		);
	}

	#[test]
	fn fingerprint_masks_the_own_name_including_recursion() {
		assert_eq!(
			prints("fn old_name(n: u32) { old_name(n - 1); }", b"old_name").text,
			prints("fn fresh(n: u32) { fresh(n - 1); }", b"fresh").text
		);
		assert_ne!(
			prints("fn old_name(n: u32) { other(n); }", b"old_name").text,
			prints("fn fresh(n: u32) { fresh(n); }", b"fresh").text
		);
	}

	#[test]
	fn fingerprint_does_not_mask_identifier_substrings() {
		assert_ne!(prints("fetch();", b"f").text, prints("getch();", b"g").text);
		assert_eq!(
			prints("fn f() { fetch(); }", b"f").text,
			prints("fn g() { fetch(); }", b"g").text
		);
	}

	#[test]
	fn body_fingerprint_starts_at_the_name() {
		let public = prints("pub fn f() { work(); }", b"f");
		let private = prints("fn f() { work(); }", b"f");
		assert_eq!(public.body, private.body);
		assert_ne!(public.text, private.text);
	}

	#[test]
	fn identity_tail_is_stable_across_anchor_paths() {
		let source =
			"struct Holder;\nimpl Holder {\n\tfn touch(&self) {}\n}\nfn free_fn(x: u32) {}\n";
		let left = environment::extract_source(Lang::Rs, source, Path::new("src/original.rs"));
		let right =
			environment::extract_source(Lang::Rs, source, Path::new("moved/deep/renamed.rs"));
		let left_tails: Vec<_> = left
			.defs()
			.map(|def| identity_tail(&def.moniker, left.root()).expect("tail under root"))
			.collect();
		let right_tails: Vec<_> = right
			.defs()
			.map(|def| identity_tail(&def.moniker, right.root()).expect("tail under root"))
			.collect();

		assert_eq!(left_tails, right_tails);
		assert!(
			left_tails.iter().any(|tail| tail
				.last()
				.is_some_and(|seg| seg.name.starts_with(b"free_fn"))),
			"expected a free_fn tail: {left_tails:?}"
		);
	}

	#[test]
	fn identity_tail_rejects_foreign_roots() {
		let source = "fn lone() {}\n";
		let graph = environment::extract_source(Lang::Rs, source, Path::new("src/a.rs"));
		let other = environment::extract_source(Lang::Rs, source, Path::new("src/b.rs"));
		let def = graph.defs().next().expect("def");

		assert!(identity_tail(&def.moniker, other.root()).is_none());
	}

	#[test]
	fn tail_parent_drops_the_final_segment() {
		let source = "struct Holder;\nimpl Holder {\n\tfn touch(&self) {}\n}\n";
		let graph = environment::extract_source(Lang::Rs, source, Path::new("src/a.rs"));
		let method = graph
			.defs()
			.find(|def| crate::code::def_kind(def) == "method")
			.expect("method def");
		let tail = identity_tail(&method.moniker, graph.root()).expect("tail");
		let parent = tail.parent();

		assert_eq!(tail.segments().len(), parent.segments().len() + 1);
		assert!(
			parent
				.last()
				.is_some_and(|seg| seg.name.as_slice() == b"Holder"),
			"parent tail should end at the impl target: {parent:?}"
		);
	}

	#[test]
	fn tail_rewrite_prefix_moves_children_under_the_new_container() {
		let source = "struct Holder;\nimpl Holder {\n\tfn touch(&self) {}\n}\nstruct Keeper;\nimpl Keeper {\n\tfn touch(&self) {}\n}\n";
		let graph = environment::extract_source(Lang::Rs, source, Path::new("src/a.rs"));
		let tails: Vec<_> = graph
			.defs()
			.filter(|def| crate::code::def_kind(def) == "method")
			.map(|def| identity_tail(&def.moniker, graph.root()).expect("tail"))
			.collect();
		let [holder_touch, keeper_touch] = tails.as_slice() else {
			panic!("expected two methods: {tails:?}");
		};

		let rewritten = holder_touch
			.rewrite_prefix(&holder_touch.parent(), &keeper_touch.parent())
			.expect("prefix applies");

		assert_eq!(&rewritten, keeper_touch);
		assert!(
			keeper_touch
				.parent()
				.rewrite_prefix(holder_touch, keeper_touch)
				.is_none(),
			"non-prefix rewrite must not apply"
		);
	}

	#[test]
	fn split_callable_name_separates_params() {
		assert_eq!(
			split_callable_name(b"findById(id:int,label:String)"),
			(
				b"findById".as_slice(),
				Some(b"(id:int,label:String)".as_slice())
			)
		);
		assert_eq!(split_callable_name(b"Holder"), (b"Holder".as_slice(), None));
	}
}