Skip to main content

code_moniker_cli/
predicate.rs

1use std::collections::BTreeSet;
2
3use code_moniker_core::core::code_graph::{CodeGraph, DefRecord, RefRecord};
4use code_moniker_core::core::kinds::{
5	KIND_COMMENT, KIND_LOCAL, KIND_MODULE, KIND_PARAM, REF_ANNOTATES, REF_CALLS, REF_DI_REGISTER,
6	REF_DI_REQUIRE, REF_EXTENDS, REF_IMPLEMENTS, REF_IMPORTS_MODULE, REF_IMPORTS_SYMBOL,
7	REF_INSTANTIATES, REF_METHOD_CALL, REF_READS, REF_REEXPORTS, REF_USES_TYPE,
8};
9use code_moniker_core::core::moniker::Moniker;
10use code_moniker_core::core::shape::Shape;
11use code_moniker_core::lang::Lang;
12
13#[derive(Clone, Debug)]
14pub enum Predicate {
15	Eq(Moniker),
16	Lt(Moniker),
17	Le(Moniker),
18	Gt(Moniker),
19	Ge(Moniker),
20	AncestorOf(Moniker),
21	DescendantOf(Moniker),
22	Bind(Moniker),
23}
24
25impl Predicate {
26	pub fn matches(&self, m: &Moniker) -> bool {
27		match self {
28			Self::Eq(o) => m == o,
29			Self::Lt(o) => m < o,
30			Self::Le(o) => m <= o,
31			Self::Gt(o) => m > o,
32			Self::Ge(o) => m >= o,
33			Self::AncestorOf(o) => m.is_ancestor_of(o),
34			Self::DescendantOf(o) => o.is_ancestor_of(m),
35			Self::Bind(o) => m.bind_match(o),
36		}
37	}
38}
39
40/// A matched ref paired with the moniker of its source def, pre-resolved at
41/// filter time so consumers don't have to carry the graph around.
42#[derive(Debug)]
43pub struct RefMatch<'g> {
44	pub record: &'g RefRecord,
45	pub source: &'g Moniker,
46}
47
48#[derive(Debug, Default)]
49pub struct MatchSet<'g> {
50	pub defs: Vec<&'g DefRecord>,
51	pub refs: Vec<RefMatch<'g>>,
52}
53
54pub fn filter<'g>(
55	graph: &'g CodeGraph,
56	predicates: &[Predicate],
57	kinds: &[String],
58	shapes: &[Shape],
59) -> MatchSet<'g> {
60	let kinds_set: Vec<&[u8]> = kinds.iter().map(|s| s.as_bytes()).collect();
61	let kind_ok = |k: &[u8]| -> bool { kinds_set.is_empty() || kinds_set.contains(&k) };
62	let shape_ok = |k: &[u8]| -> bool { shapes.is_empty() || shapes.contains(&Shape::for_kind(k)) };
63	let mut defs: Vec<&DefRecord> = graph
64		.defs()
65		.filter(|d| {
66			kind_ok(&d.kind)
67				&& shape_ok(&d.kind)
68				&& predicates.iter().all(|p| p.matches(&d.moniker))
69		})
70		.collect();
71	let refs: Vec<&RefRecord> = graph
72		.refs()
73		.filter(|r| {
74			kind_ok(&r.kind) && shape_ok(&r.kind) && predicates.iter().all(|p| p.matches(&r.target))
75		})
76		.collect();
77	defs.sort_by(|a, b| a.moniker.as_bytes().cmp(b.moniker.as_bytes()));
78	let mut keyed: Vec<RefMatch<'g>> = refs
79		.into_iter()
80		.map(|r| RefMatch {
81			record: r,
82			source: &graph.def_at(r.source).moniker,
83		})
84		.collect();
85	keyed.sort_by(|a, b| {
86		(
87			a.source.as_bytes(),
88			a.record.target.as_bytes(),
89			a.record.position,
90		)
91			.cmp(&(
92				b.source.as_bytes(),
93				b.record.target.as_bytes(),
94				b.record.position,
95			))
96	});
97	MatchSet { defs, refs: keyed }
98}
99
100pub const CROSS_LANG_KINDS: &[&[u8]] = &[
101	KIND_MODULE,
102	KIND_COMMENT,
103	KIND_LOCAL,
104	KIND_PARAM,
105	REF_IMPORTS_SYMBOL,
106	REF_IMPORTS_MODULE,
107	REF_REEXPORTS,
108	REF_DI_REGISTER,
109	REF_DI_REQUIRE,
110	REF_CALLS,
111	REF_METHOD_CALL,
112	REF_READS,
113	REF_USES_TYPE,
114	REF_INSTANTIATES,
115	REF_EXTENDS,
116	REF_IMPLEMENTS,
117	REF_ANNOTATES,
118];
119
120pub fn known_kinds<'a>(langs: impl IntoIterator<Item = &'a Lang>) -> BTreeSet<&'static str> {
121	let mut out: BTreeSet<&'static str> = BTreeSet::new();
122	for k in CROSS_LANG_KINDS {
123		out.insert(std::str::from_utf8(k).expect("kind constants are ASCII"));
124	}
125	for lang in langs {
126		for k in lang.allowed_kinds() {
127			out.insert(*k);
128		}
129	}
130	out
131}
132
133pub fn unknown_kinds(kinds: &[String], known: &BTreeSet<&'static str>) -> Vec<String> {
134	kinds
135		.iter()
136		.filter(|k| !known.contains(k.as_str()))
137		.cloned()
138		.collect()
139}
140
141#[cfg(test)]
142mod tests {
143	use super::*;
144	use code_moniker_core::core::moniker::MonikerBuilder;
145
146	fn m(segments: &[(&[u8], &[u8])]) -> Moniker {
147		let mut b = MonikerBuilder::new();
148		b.project(b"app");
149		for (k, n) in segments {
150			b.segment(k, n);
151		}
152		b.build()
153	}
154
155	fn build_graph() -> CodeGraph {
156		let root = m(&[]);
157		let mut g = CodeGraph::new(root.clone(), b"module");
158		let foo = m(&[(b"class", b"Foo")]);
159		let bar = m(&[(b"class", b"Foo"), (b"method", b"bar")]);
160		let baz = m(&[(b"class", b"Baz")]);
161		g.add_def(foo.clone(), b"class", &root, Some((1, 0)))
162			.unwrap();
163		g.add_def(bar, b"method", &foo, Some((2, 2))).unwrap();
164		g.add_def(baz.clone(), b"class", &root, Some((10, 0)))
165			.unwrap();
166		g.add_ref(&baz, foo, b"EXTENDS", Some((10, 14))).unwrap();
167		g
168	}
169
170	#[test]
171	fn no_predicate_matches_everything() {
172		let g = build_graph();
173		let r = filter(&g, &[], &[], &[]);
174		assert_eq!(r.defs.len(), 4);
175		assert_eq!(r.refs.len(), 1);
176	}
177
178	#[test]
179	fn kind_filter_or_combines() {
180		let g = build_graph();
181		let r = filter(&g, &[], &["method".to_string()], &[]);
182		assert_eq!(r.defs.len(), 1);
183		assert_eq!(r.defs[0].kind, b"method");
184		let r = filter(&g, &[], &["method".to_string(), "module".to_string()], &[]);
185		assert_eq!(r.defs.len(), 2);
186	}
187
188	#[test]
189	fn descendant_of_keeps_only_strict_descendants_and_target() {
190		let g = build_graph();
191		let foo = m(&[(b"class", b"Foo")]);
192		let r = filter(&g, &[Predicate::DescendantOf(foo)], &[], &[]);
193		let names: Vec<&[u8]> = r.defs.iter().map(|d| d.kind.as_slice()).collect();
194		assert!(names.contains(&b"class".as_slice()));
195		assert!(names.contains(&b"method".as_slice()));
196		assert_eq!(r.defs.len(), 2);
197	}
198
199	#[test]
200	fn equality_matches_one_def() {
201		let g = build_graph();
202		let foo = m(&[(b"class", b"Foo")]);
203		let r = filter(&g, &[Predicate::Eq(foo.clone())], &[], &[]);
204		assert_eq!(r.defs.len(), 1);
205		assert_eq!(&r.defs[0].moniker, &foo);
206		assert_eq!(r.refs.len(), 1, "ref to Foo also matches via target");
207	}
208
209	#[test]
210	fn ordering_predicates_use_byte_lex() {
211		let g = build_graph();
212		let baz = m(&[(b"class", b"Baz")]);
213		let r = filter(&g, &[Predicate::Lt(baz.clone())], &[], &[]);
214		assert!(r.defs.iter().all(|d| d.moniker < baz));
215		let r = filter(&g, &[Predicate::Ge(baz.clone())], &[], &[]);
216		assert!(r.defs.iter().all(|d| d.moniker >= baz));
217	}
218
219	#[test]
220	fn ancestor_of_includes_self() {
221		let g = build_graph();
222		let bar = m(&[(b"class", b"Foo"), (b"method", b"bar")]);
223		let r = filter(&g, &[Predicate::AncestorOf(bar)], &[], &[]);
224		let kinds: Vec<&[u8]> = r.defs.iter().map(|d| d.kind.as_slice()).collect();
225		assert!(kinds.contains(&b"module".as_slice()));
226		assert!(kinds.contains(&b"class".as_slice()));
227		assert!(kinds.contains(&b"method".as_slice()));
228	}
229
230	#[test]
231	fn predicate_and_kind_compose() {
232		let g = build_graph();
233		let foo = m(&[(b"class", b"Foo")]);
234		let r = filter(
235			&g,
236			&[Predicate::DescendantOf(foo)],
237			&["method".to_string()],
238			&[],
239		);
240		assert_eq!(r.defs.len(), 1);
241		assert_eq!(r.defs[0].kind, b"method");
242	}
243
244	#[test]
245	fn ref_filtered_by_target_moniker() {
246		let g = build_graph();
247		let foo = m(&[(b"class", b"Foo")]);
248		let r = filter(&g, &[Predicate::Eq(foo)], &[], &[]);
249		assert_eq!(r.refs.len(), 1, "EXTENDS ref targets Foo");
250	}
251
252	#[test]
253	fn shape_filter_picks_callable_across_kinds() {
254		let g = build_graph();
255		let r = filter(&g, &[], &[], &[Shape::Callable]);
256		assert_eq!(r.defs.len(), 1, "only `method` has shape callable");
257		assert_eq!(r.defs[0].kind, b"method");
258	}
259
260	#[test]
261	fn shape_filter_ref_isolates_ref_records() {
262		let g = build_graph();
263		let r = filter(&g, &[], &[], &[Shape::Ref]);
264		assert!(r.defs.is_empty(), "no def has shape `ref`");
265		assert_eq!(r.refs.len(), 1);
266	}
267
268	#[test]
269	fn kind_and_shape_compose_as_and() {
270		let g = build_graph();
271		let r = filter(&g, &[], &["method".to_string()], &[Shape::Type]);
272		assert!(
273			r.defs.is_empty(),
274			"method is callable, not type — empty AND"
275		);
276	}
277
278	#[test]
279	fn shape_for_kind_maps_known_and_unknown() {
280		assert_eq!(Shape::for_kind(b"method"), Shape::Callable);
281		assert_eq!(Shape::for_kind(b"class"), Shape::Type);
282		assert_eq!(Shape::for_kind(b"EXTENDS"), Shape::Ref);
283	}
284
285	#[test]
286	fn known_kinds_for_ts_includes_class_function_and_ref_kinds() {
287		let k = known_kinds(std::iter::once(&Lang::Ts));
288		assert!(k.contains("class"));
289		assert!(k.contains("function"));
290		assert!(k.contains("method"));
291		assert!(k.contains("calls"));
292		assert!(k.contains("imports_module"));
293		assert!(k.contains("module"));
294		assert!(!k.contains("fn"), "fn is Rust-specific, not in ts vocab");
295	}
296
297	#[test]
298	fn known_kinds_union_picks_up_per_lang_specifics() {
299		let langs = [Lang::Ts, Lang::Rs];
300		let k = known_kinds(langs.iter());
301		assert!(k.contains("function"), "TS contributes `function`");
302		assert!(k.contains("fn"), "Rust contributes `fn`");
303	}
304
305	#[test]
306	fn unknown_kinds_flags_typos_and_lang_mismatches() {
307		let langs = [Lang::Ts];
308		let k = known_kinds(langs.iter());
309		let unknown = unknown_kinds(
310			&[
311				"function".to_string(),
312				"fn".to_string(),
313				"clazz".to_string(),
314			],
315			&k,
316		);
317		assert_eq!(unknown, vec!["fn".to_string(), "clazz".to_string()]);
318	}
319
320	#[test]
321	fn defs_sorted_by_moniker_bytes() {
322		let g = build_graph();
323		let r = filter(&g, &[], &[], &[]);
324		let mut prev: Option<&[u8]> = None;
325		for d in &r.defs {
326			let cur = d.moniker.as_bytes();
327			if let Some(p) = prev {
328				assert!(p <= cur, "defs not sorted: {p:?} then {cur:?}");
329			}
330			prev = Some(cur);
331		}
332	}
333}