Skip to main content

code_moniker_cli/extract/
filter.rs

1use code_moniker_core::core::code_graph::{CodeGraph, DefRecord, RefRecord};
2use code_moniker_core::core::moniker::Moniker;
3use code_moniker_core::core::moniker::query::bare_callable_name;
4use code_moniker_core::core::shape::Shape;
5use regex::Regex;
6
7#[derive(Clone, Debug)]
8pub enum Predicate {
9	Eq(Moniker),
10	Lt(Moniker),
11	Le(Moniker),
12	Gt(Moniker),
13	Ge(Moniker),
14	AncestorOf(Moniker),
15	DescendantOf(Moniker),
16	Bind(Moniker),
17}
18
19impl Predicate {
20	pub fn matches(&self, m: &Moniker) -> bool {
21		match self {
22			Self::Eq(o) => m == o,
23			Self::Lt(o) => m < o,
24			Self::Le(o) => m <= o,
25			Self::Gt(o) => m > o,
26			Self::Ge(o) => m >= o,
27			Self::AncestorOf(o) => m.is_ancestor_of(o),
28			Self::DescendantOf(o) => o.is_ancestor_of(m),
29			Self::Bind(o) => m.bind_match(o),
30		}
31	}
32}
33
34/// A matched ref paired with the moniker of its source def, pre-resolved at
35/// filter time so consumers don't have to carry the graph around.
36#[derive(Debug)]
37pub struct RefMatch<'g> {
38	pub record: &'g RefRecord,
39	pub source: &'g Moniker,
40}
41
42#[derive(Debug, Default)]
43pub struct MatchSet<'g> {
44	pub defs: Vec<&'g DefRecord>,
45	pub refs: Vec<RefMatch<'g>>,
46}
47
48pub fn filter<'g>(
49	graph: &'g CodeGraph,
50	predicates: &[Predicate],
51	kinds: &[String],
52	names: &[Regex],
53	shapes: &[Shape],
54) -> MatchSet<'g> {
55	let kinds_set: Vec<&[u8]> = kinds.iter().map(|s| s.as_bytes()).collect();
56	let kind_ok = |k: &[u8]| -> bool { kinds_set.is_empty() || kinds_set.contains(&k) };
57	let shape_ok = |k: &[u8]| -> bool { shapes.is_empty() || shapes.contains(&Shape::for_kind(k)) };
58	let name_ok = |m: &Moniker| -> bool {
59		names.is_empty() || name_of(m).is_some_and(|name| names.iter().any(|re| re.is_match(&name)))
60	};
61	let mut defs: Vec<&DefRecord> = graph
62		.defs()
63		.filter(|d| {
64			kind_ok(&d.kind)
65				&& shape_ok(&d.kind)
66				&& name_ok(&d.moniker)
67				&& predicates.iter().all(|p| p.matches(&d.moniker))
68		})
69		.collect();
70	let refs: Vec<&RefRecord> = graph
71		.refs()
72		.filter(|r| {
73			kind_ok(&r.kind)
74				&& shape_ok(&r.kind)
75				&& name_ok(&r.target)
76				&& predicates.iter().all(|p| p.matches(&r.target))
77		})
78		.collect();
79	defs.sort_by(|a, b| a.moniker.as_encoded().cmp(b.moniker.as_encoded()));
80	let mut keyed: Vec<RefMatch<'g>> = refs
81		.into_iter()
82		.map(|r| RefMatch {
83			record: r,
84			source: &graph.def_at(r.source).moniker,
85		})
86		.collect();
87	keyed.sort_by(|a, b| {
88		(
89			a.source.as_encoded(),
90			a.record.target.as_encoded(),
91			a.record.position,
92		)
93			.cmp(&(
94				b.source.as_encoded(),
95				b.record.target.as_encoded(),
96				b.record.position,
97			))
98	});
99	MatchSet { defs, refs: keyed }
100}
101
102pub fn compile_name_filters(names: &[String]) -> anyhow::Result<Vec<Regex>> {
103	names
104		.iter()
105		.map(|name| {
106			Regex::new(name).map_err(|e| anyhow::anyhow!("invalid --name regex `{name}`: {e}"))
107		})
108		.collect()
109}
110
111fn name_of(m: &Moniker) -> Option<String> {
112	let last = m.as_view().segments().last()?;
113	let bare = bare_callable_name(last.name);
114	std::str::from_utf8(bare).ok().map(ToOwned::to_owned)
115}
116
117#[cfg(test)]
118mod tests {
119	use super::*;
120	use code_moniker_core::core::moniker::MonikerBuilder;
121
122	fn m(segments: &[(&[u8], &[u8])]) -> Moniker {
123		let mut b = MonikerBuilder::new();
124		b.project(b"app");
125		for (k, n) in segments {
126			b.segment(k, n);
127		}
128		b.build()
129	}
130
131	fn build_graph() -> CodeGraph {
132		let root = m(&[]);
133		let mut g = CodeGraph::new(root.clone(), b"module");
134		let foo = m(&[(b"class", b"Foo")]);
135		let bar = m(&[(b"class", b"Foo"), (b"method", b"bar")]);
136		let baz = m(&[(b"class", b"Baz")]);
137		g.add_def(foo.clone(), b"class", &root, Some((1, 0)))
138			.unwrap();
139		g.add_def(bar, b"method", &foo, Some((2, 2))).unwrap();
140		g.add_def(baz.clone(), b"class", &root, Some((10, 0)))
141			.unwrap();
142		g.add_ref(&baz, foo, b"EXTENDS", Some((10, 14))).unwrap();
143		g
144	}
145
146	#[test]
147	fn no_predicate_matches_everything() {
148		let g = build_graph();
149		let r = filter(&g, &[], &[], &[], &[]);
150		assert_eq!(r.defs.len(), 4);
151		assert_eq!(r.refs.len(), 1);
152	}
153
154	#[test]
155	fn kind_filter_or_combines() {
156		let g = build_graph();
157		let r = filter(&g, &[], &["method".to_string()], &[], &[]);
158		assert_eq!(r.defs.len(), 1);
159		assert_eq!(r.defs[0].kind, b"method");
160		let r = filter(
161			&g,
162			&[],
163			&["method".to_string(), "module".to_string()],
164			&[],
165			&[],
166		);
167		assert_eq!(r.defs.len(), 2);
168	}
169
170	#[test]
171	fn name_regex_filter_matches_last_segment_name() {
172		let g = build_graph();
173		let names = compile_name_filters(&["^Ba".to_string()]).unwrap();
174		let r = filter(&g, &[], &[], &names, &[]);
175		assert_eq!(r.defs.len(), 1);
176		assert_eq!(name_of(&r.defs[0].moniker).as_deref(), Some("Baz"));
177	}
178
179	#[test]
180	fn descendant_of_keeps_only_strict_descendants_and_target() {
181		let g = build_graph();
182		let foo = m(&[(b"class", b"Foo")]);
183		let r = filter(&g, &[Predicate::DescendantOf(foo)], &[], &[], &[]);
184		let names: Vec<&[u8]> = r.defs.iter().map(|d| d.kind.as_ref()).collect();
185		assert!(names.contains(&b"class".as_slice()));
186		assert!(names.contains(&b"method".as_slice()));
187		assert_eq!(r.defs.len(), 2);
188	}
189
190	#[test]
191	fn equality_matches_one_def() {
192		let g = build_graph();
193		let foo = m(&[(b"class", b"Foo")]);
194		let r = filter(&g, &[Predicate::Eq(foo.clone())], &[], &[], &[]);
195		assert_eq!(r.defs.len(), 1);
196		assert_eq!(&r.defs[0].moniker, &foo);
197		assert_eq!(r.refs.len(), 1, "ref to Foo also matches via target");
198	}
199
200	#[test]
201	fn ordering_predicates_use_byte_lex() {
202		let g = build_graph();
203		let baz = m(&[(b"class", b"Baz")]);
204		let r = filter(&g, &[Predicate::Lt(baz.clone())], &[], &[], &[]);
205		assert!(r.defs.iter().all(|d| d.moniker < baz));
206		let r = filter(&g, &[Predicate::Ge(baz.clone())], &[], &[], &[]);
207		assert!(r.defs.iter().all(|d| d.moniker >= baz));
208	}
209
210	#[test]
211	fn ancestor_of_includes_self() {
212		let g = build_graph();
213		let bar = m(&[(b"class", b"Foo"), (b"method", b"bar")]);
214		let r = filter(&g, &[Predicate::AncestorOf(bar)], &[], &[], &[]);
215		let kinds: Vec<&[u8]> = r.defs.iter().map(|d| d.kind.as_ref()).collect();
216		assert!(kinds.contains(&b"module".as_slice()));
217		assert!(kinds.contains(&b"class".as_slice()));
218		assert!(kinds.contains(&b"method".as_slice()));
219	}
220
221	#[test]
222	fn predicate_and_kind_compose() {
223		let g = build_graph();
224		let foo = m(&[(b"class", b"Foo")]);
225		let r = filter(
226			&g,
227			&[Predicate::DescendantOf(foo)],
228			&["method".to_string()],
229			&[],
230			&[],
231		);
232		assert_eq!(r.defs.len(), 1);
233		assert_eq!(r.defs[0].kind, b"method");
234	}
235
236	#[test]
237	fn ref_filtered_by_target_moniker() {
238		let g = build_graph();
239		let foo = m(&[(b"class", b"Foo")]);
240		let r = filter(&g, &[Predicate::Eq(foo)], &[], &[], &[]);
241		assert_eq!(r.refs.len(), 1, "EXTENDS ref targets Foo");
242	}
243
244	#[test]
245	fn shape_filter_picks_callable_across_kinds() {
246		let g = build_graph();
247		let r = filter(&g, &[], &[], &[], &[Shape::Callable]);
248		assert_eq!(r.defs.len(), 1, "only `method` has shape callable");
249		assert_eq!(r.defs[0].kind, b"method");
250	}
251
252	#[test]
253	fn shape_filter_ref_isolates_ref_records() {
254		let g = build_graph();
255		let r = filter(&g, &[], &[], &[], &[Shape::Ref]);
256		assert!(r.defs.is_empty(), "no def has shape `ref`");
257		assert_eq!(r.refs.len(), 1);
258	}
259
260	#[test]
261	fn kind_and_shape_compose_as_and() {
262		let g = build_graph();
263		let r = filter(&g, &[], &["method".to_string()], &[], &[Shape::Type]);
264		assert!(
265			r.defs.is_empty(),
266			"method is callable, not type — empty AND"
267		);
268	}
269
270	#[test]
271	fn shape_for_kind_maps_known_and_unknown() {
272		assert_eq!(Shape::for_kind(b"method"), Shape::Callable);
273		assert_eq!(Shape::for_kind(b"class"), Shape::Type);
274		assert_eq!(Shape::for_kind(b"EXTENDS"), Shape::Ref);
275	}
276
277	#[test]
278	fn defs_sorted_by_moniker_bytes() {
279		let g = build_graph();
280		let r = filter(&g, &[], &[], &[], &[]);
281		let mut prev: Option<&[u8]> = None;
282		for d in &r.defs {
283			let cur = d.moniker.as_encoded();
284			if let Some(p) = prev {
285				assert!(p <= cur, "defs not sorted: {p:?} then {cur:?}");
286			}
287			prev = Some(cur);
288		}
289	}
290}