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