code-moniker 0.4.0

Standalone CLI / linter for the code-moniker symbol graph: file and directory probes, project-wide architecture rules.
Documentation
use code_moniker_core::core::code_graph::{CodeGraph, DefRecord, RefRecord};
use code_moniker_core::core::moniker::Moniker;
use code_moniker_core::core::moniker::query::bare_callable_name;
use code_moniker_core::core::shape::Shape;
use regex::Regex;

#[derive(Clone, Debug)]
pub enum Predicate {
	Eq(Moniker),
	Lt(Moniker),
	Le(Moniker),
	Gt(Moniker),
	Ge(Moniker),
	AncestorOf(Moniker),
	DescendantOf(Moniker),
	Bind(Moniker),
}

impl Predicate {
	pub fn matches(&self, m: &Moniker) -> bool {
		match self {
			Self::Eq(o) => m == o,
			Self::Lt(o) => m < o,
			Self::Le(o) => m <= o,
			Self::Gt(o) => m > o,
			Self::Ge(o) => m >= o,
			Self::AncestorOf(o) => m.is_ancestor_of(o),
			Self::DescendantOf(o) => o.is_ancestor_of(m),
			Self::Bind(o) => m.bind_match(o),
		}
	}
}

/// A matched ref paired with the moniker of its source def, pre-resolved at
/// filter time so consumers don't have to carry the graph around.
#[derive(Debug)]
pub struct RefMatch<'g> {
	pub record: &'g RefRecord,
	pub source: &'g Moniker,
}

#[derive(Debug, Default)]
pub struct MatchSet<'g> {
	pub defs: Vec<&'g DefRecord>,
	pub refs: Vec<RefMatch<'g>>,
}

pub fn filter<'g>(
	graph: &'g CodeGraph,
	predicates: &[Predicate],
	kinds: &[String],
	names: &[Regex],
	shapes: &[Shape],
) -> MatchSet<'g> {
	let kinds_set: Vec<&[u8]> = kinds.iter().map(|s| s.as_bytes()).collect();
	let kind_ok = |k: &[u8]| -> bool { kinds_set.is_empty() || kinds_set.contains(&k) };
	let shape_ok = |k: &[u8]| -> bool { shapes.is_empty() || shapes.contains(&Shape::for_kind(k)) };
	let name_ok = |m: &Moniker| -> bool {
		names.is_empty() || name_of(m).is_some_and(|name| names.iter().any(|re| re.is_match(&name)))
	};
	let mut defs: Vec<&DefRecord> = graph
		.defs()
		.filter(|d| {
			kind_ok(&d.kind)
				&& shape_ok(&d.kind)
				&& name_ok(&d.moniker)
				&& predicates.iter().all(|p| p.matches(&d.moniker))
		})
		.collect();
	let refs: Vec<&RefRecord> = graph
		.refs()
		.filter(|r| {
			kind_ok(&r.kind)
				&& shape_ok(&r.kind)
				&& name_ok(&r.target)
				&& predicates.iter().all(|p| p.matches(&r.target))
		})
		.collect();
	defs.sort_by(|a, b| a.moniker.as_encoded().cmp(b.moniker.as_encoded()));
	let mut keyed: Vec<RefMatch<'g>> = refs
		.into_iter()
		.map(|r| RefMatch {
			record: r,
			source: &graph.def_at(r.source).moniker,
		})
		.collect();
	keyed.sort_by(|a, b| {
		(
			a.source.as_encoded(),
			a.record.target.as_encoded(),
			a.record.position,
		)
			.cmp(&(
				b.source.as_encoded(),
				b.record.target.as_encoded(),
				b.record.position,
			))
	});
	MatchSet { defs, refs: keyed }
}

pub fn compile_name_filters(names: &[String]) -> anyhow::Result<Vec<Regex>> {
	names
		.iter()
		.map(|name| {
			Regex::new(name).map_err(|e| anyhow::anyhow!("invalid --name regex `{name}`: {e}"))
		})
		.collect()
}

fn name_of(m: &Moniker) -> Option<String> {
	let last = m.as_view().segments().last()?;
	let bare = bare_callable_name(last.name);
	std::str::from_utf8(bare).ok().map(ToOwned::to_owned)
}

#[cfg(test)]
mod tests {
	use super::*;
	use code_moniker_core::core::moniker::MonikerBuilder;

	fn m(segments: &[(&[u8], &[u8])]) -> Moniker {
		let mut b = MonikerBuilder::new();
		b.project(b"app");
		for (k, n) in segments {
			b.segment(k, n);
		}
		b.build()
	}

	fn build_graph() -> CodeGraph {
		let root = m(&[]);
		let mut g = CodeGraph::new(root.clone(), b"module");
		let foo = m(&[(b"class", b"Foo")]);
		let bar = m(&[(b"class", b"Foo"), (b"method", b"bar")]);
		let baz = m(&[(b"class", b"Baz")]);
		g.add_def(foo.clone(), b"class", &root, Some((1, 0)))
			.unwrap();
		g.add_def(bar, b"method", &foo, Some((2, 2))).unwrap();
		g.add_def(baz.clone(), b"class", &root, Some((10, 0)))
			.unwrap();
		g.add_ref(&baz, foo, b"EXTENDS", Some((10, 14))).unwrap();
		g
	}

	#[test]
	fn no_predicate_matches_everything() {
		let g = build_graph();
		let r = filter(&g, &[], &[], &[], &[]);
		assert_eq!(r.defs.len(), 4);
		assert_eq!(r.refs.len(), 1);
	}

	#[test]
	fn kind_filter_or_combines() {
		let g = build_graph();
		let r = filter(&g, &[], &["method".to_string()], &[], &[]);
		assert_eq!(r.defs.len(), 1);
		assert_eq!(r.defs[0].kind, b"method");
		let r = filter(
			&g,
			&[],
			&["method".to_string(), "module".to_string()],
			&[],
			&[],
		);
		assert_eq!(r.defs.len(), 2);
	}

	#[test]
	fn name_regex_filter_matches_last_segment_name() {
		let g = build_graph();
		let names = compile_name_filters(&["^Ba".to_string()]).unwrap();
		let r = filter(&g, &[], &[], &names, &[]);
		assert_eq!(r.defs.len(), 1);
		assert_eq!(name_of(&r.defs[0].moniker).as_deref(), Some("Baz"));
	}

	#[test]
	fn descendant_of_keeps_only_strict_descendants_and_target() {
		let g = build_graph();
		let foo = m(&[(b"class", b"Foo")]);
		let r = filter(&g, &[Predicate::DescendantOf(foo)], &[], &[], &[]);
		let names: Vec<&[u8]> = r.defs.iter().map(|d| d.kind.as_ref()).collect();
		assert!(names.contains(&b"class".as_slice()));
		assert!(names.contains(&b"method".as_slice()));
		assert_eq!(r.defs.len(), 2);
	}

	#[test]
	fn equality_matches_one_def() {
		let g = build_graph();
		let foo = m(&[(b"class", b"Foo")]);
		let r = filter(&g, &[Predicate::Eq(foo.clone())], &[], &[], &[]);
		assert_eq!(r.defs.len(), 1);
		assert_eq!(&r.defs[0].moniker, &foo);
		assert_eq!(r.refs.len(), 1, "ref to Foo also matches via target");
	}

	#[test]
	fn ordering_predicates_use_byte_lex() {
		let g = build_graph();
		let baz = m(&[(b"class", b"Baz")]);
		let r = filter(&g, &[Predicate::Lt(baz.clone())], &[], &[], &[]);
		assert!(r.defs.iter().all(|d| d.moniker < baz));
		let r = filter(&g, &[Predicate::Ge(baz.clone())], &[], &[], &[]);
		assert!(r.defs.iter().all(|d| d.moniker >= baz));
	}

	#[test]
	fn ancestor_of_includes_self() {
		let g = build_graph();
		let bar = m(&[(b"class", b"Foo"), (b"method", b"bar")]);
		let r = filter(&g, &[Predicate::AncestorOf(bar)], &[], &[], &[]);
		let kinds: Vec<&[u8]> = r.defs.iter().map(|d| d.kind.as_ref()).collect();
		assert!(kinds.contains(&b"module".as_slice()));
		assert!(kinds.contains(&b"class".as_slice()));
		assert!(kinds.contains(&b"method".as_slice()));
	}

	#[test]
	fn predicate_and_kind_compose() {
		let g = build_graph();
		let foo = m(&[(b"class", b"Foo")]);
		let r = filter(
			&g,
			&[Predicate::DescendantOf(foo)],
			&["method".to_string()],
			&[],
			&[],
		);
		assert_eq!(r.defs.len(), 1);
		assert_eq!(r.defs[0].kind, b"method");
	}

	#[test]
	fn ref_filtered_by_target_moniker() {
		let g = build_graph();
		let foo = m(&[(b"class", b"Foo")]);
		let r = filter(&g, &[Predicate::Eq(foo)], &[], &[], &[]);
		assert_eq!(r.refs.len(), 1, "EXTENDS ref targets Foo");
	}

	#[test]
	fn shape_filter_picks_callable_across_kinds() {
		let g = build_graph();
		let r = filter(&g, &[], &[], &[], &[Shape::Callable]);
		assert_eq!(r.defs.len(), 1, "only `method` has shape callable");
		assert_eq!(r.defs[0].kind, b"method");
	}

	#[test]
	fn shape_filter_ref_isolates_ref_records() {
		let g = build_graph();
		let r = filter(&g, &[], &[], &[], &[Shape::Ref]);
		assert!(r.defs.is_empty(), "no def has shape `ref`");
		assert_eq!(r.refs.len(), 1);
	}

	#[test]
	fn kind_and_shape_compose_as_and() {
		let g = build_graph();
		let r = filter(&g, &[], &["method".to_string()], &[], &[Shape::Type]);
		assert!(
			r.defs.is_empty(),
			"method is callable, not type — empty AND"
		);
	}

	#[test]
	fn shape_for_kind_maps_known_and_unknown() {
		assert_eq!(Shape::for_kind(b"method"), Shape::Callable);
		assert_eq!(Shape::for_kind(b"class"), Shape::Type);
		assert_eq!(Shape::for_kind(b"EXTENDS"), Shape::Ref);
	}

	#[test]
	fn defs_sorted_by_moniker_bytes() {
		let g = build_graph();
		let r = filter(&g, &[], &[], &[], &[]);
		let mut prev: Option<&[u8]> = None;
		for d in &r.defs {
			let cur = d.moniker.as_encoded();
			if let Some(p) = prev {
				assert!(p <= cur, "defs not sorted: {p:?} then {cur:?}");
			}
			prev = Some(cur);
		}
	}
}