Skip to main content

code_moniker_core/declare/
build.rs

1use std::collections::HashSet;
2
3use super::{DeclSymbol, DeclareError, DeclareSpec, EdgeKind};
4use crate::core::code_graph::{CodeGraph, DefAttrs, RefAttrs};
5use crate::core::kinds::{
6	BIND_INJECT, BIND_LOCAL, BIND_NONE, ORIGIN_DECLARED, REF_CALLS, REF_DI_REGISTER,
7	REF_DI_REQUIRE, REF_IMPORTS_MODULE,
8};
9use crate::core::moniker::{Moniker, MonikerBuilder};
10use crate::core::uri::{UriConfig, to_uri};
11
12pub fn build_graph(spec: &DeclareSpec) -> Result<CodeGraph, DeclareError> {
13	let mut declared: HashSet<Moniker> = HashSet::with_capacity(spec.symbols.len() + 1);
14	declared.insert(spec.root.clone());
15	for (i, sym) in spec.symbols.iter().enumerate() {
16		validate_kind_agreement(sym, i)?;
17		if !declared.insert(sym.moniker.clone()) {
18			return Err(DeclareError::DuplicateMoniker {
19				moniker: render_uri(&sym.moniker),
20			});
21		}
22	}
23
24	for (i, sym) in spec.symbols.iter().enumerate() {
25		if !declared.contains(&sym.parent) {
26			return Err(DeclareError::UnknownParent {
27				path: format!("$.symbols[{i}].parent"),
28				parent: render_uri(&sym.parent),
29			});
30		}
31	}
32
33	let mut ordered: Vec<&DeclSymbol> = spec.symbols.iter().collect();
34	ordered.sort_by_key(|s| s.moniker.as_bytes().len());
35
36	let mut graph = CodeGraph::new(spec.root.clone(), b"module");
37
38	for sym in &ordered {
39		let attrs = DefAttrs {
40			visibility: sym.visibility.as_deref().unwrap_or("").as_bytes(),
41			signature: sym.signature.as_deref().unwrap_or("").as_bytes(),
42			binding: b"",
43			origin: ORIGIN_DECLARED,
44		};
45		graph
46			.add_def_attrs(
47				sym.moniker.clone(),
48				sym.kind.as_bytes(),
49				&sym.parent,
50				None,
51				&attrs,
52			)
53			.map_err(|e| DeclareError::GraphError(e.to_string()))?;
54	}
55
56	for (i, edge) in spec.edges.iter().enumerate() {
57		if !declared.contains(&edge.from) {
58			return Err(DeclareError::UnknownEdgeSource {
59				path: format!("$.edges[{i}].from"),
60				from: render_uri(&edge.from),
61			});
62		}
63		let (ref_kind, binding_override) = lower_edge(edge.kind, &edge.from, &edge.to);
64		let attrs = RefAttrs {
65			binding: binding_override,
66			..RefAttrs::default()
67		};
68		graph
69			.add_ref_attrs(&edge.from, edge.to.clone(), ref_kind, None, &attrs)
70			.map_err(|e| DeclareError::GraphError(e.to_string()))?;
71	}
72
73	Ok(graph)
74}
75
76fn validate_kind_agreement(sym: &DeclSymbol, idx: usize) -> Result<(), DeclareError> {
77	let last_kind = sym
78		.moniker
79		.last_kind()
80		.ok_or_else(|| DeclareError::InvalidMoniker {
81			path: format!("$.symbols[{idx}].moniker"),
82			value: render_uri(&sym.moniker),
83			reason: "moniker has no segments (cannot extract last kind)".to_string(),
84		})?;
85	let last_kind_str =
86		std::str::from_utf8(&last_kind).map_err(|_| DeclareError::InvalidMoniker {
87			path: format!("$.symbols[{idx}].moniker"),
88			value: render_uri(&sym.moniker),
89			reason: "last segment kind is not UTF-8".to_string(),
90		})?;
91	if last_kind_str != sym.kind {
92		return Err(DeclareError::KindMismatchMoniker {
93			path: format!("$.symbols[{idx}]"),
94			declared_kind: sym.kind.clone(),
95			moniker_last_kind: last_kind_str.to_string(),
96		});
97	}
98	Ok(())
99}
100
101fn lower_edge(kind: EdgeKind, from: &Moniker, to: &Moniker) -> (&'static [u8], &'static [u8]) {
102	match kind {
103		EdgeKind::DependsOn => (REF_IMPORTS_MODULE, b""),
104		EdgeKind::Calls => {
105			let binding = if shares_module(from, to) {
106				BIND_LOCAL
107			} else {
108				BIND_NONE
109			};
110			(REF_CALLS, binding)
111		}
112		EdgeKind::InjectsProvide => (REF_DI_REGISTER, BIND_INJECT),
113		EdgeKind::InjectsRequire => (REF_DI_REQUIRE, BIND_INJECT),
114	}
115}
116
117fn shares_module(a: &Moniker, b: &Moniker) -> bool {
118	let am = module_anchor_bytes(a);
119	let bm = module_anchor_bytes(b);
120	match (am, bm) {
121		(Some(x), Some(y)) => x == y,
122		_ => false,
123	}
124}
125
126fn module_anchor_bytes(m: &Moniker) -> Option<Vec<u8>> {
127	let view = m.as_view();
128	let mut anchor = MonikerBuilder::new();
129	anchor.project(view.project());
130	let mut found = false;
131	for seg in view.segments() {
132		anchor.segment(seg.kind, seg.name);
133		if seg.kind == b"module" {
134			found = true;
135			break;
136		}
137	}
138	if found {
139		Some(anchor.build().into_bytes())
140	} else {
141		None
142	}
143}
144
145fn render_uri(m: &Moniker) -> String {
146	let cfg = UriConfig::default();
147	to_uri(m, &cfg).unwrap_or_else(|_| format!("{:?}", m.as_bytes()))
148}
149
150#[cfg(test)]
151mod tests {
152	use super::*;
153	use crate::core::kinds::{ORIGIN_DECLARED, ORIGIN_EXTRACTED};
154	use crate::core::moniker::MonikerBuilder;
155	use crate::declare::{parse_moniker_uri, parse_spec};
156	use serde_json::json;
157
158	fn parse_uri(uri: &str) -> Moniker {
159		parse_moniker_uri(uri).unwrap()
160	}
161
162	fn build_from_json(v: serde_json::Value) -> Result<CodeGraph, DeclareError> {
163		let spec = parse_spec(&v)?;
164		build_graph(&spec)
165	}
166
167	fn java_minimal() -> serde_json::Value {
168		json!({
169			"root": "code+moniker://app/srcset:main/lang:java/package:com/module:Foo",
170			"lang": "java",
171			"symbols": [
172				{
173					"moniker": "code+moniker://app/srcset:main/lang:java/package:com/module:Foo/class:Foo",
174					"kind": "class",
175					"parent": "code+moniker://app/srcset:main/lang:java/package:com/module:Foo",
176					"visibility": "public"
177				}
178			]
179		})
180	}
181
182	#[test]
183	fn build_minimal_spec_yields_root_plus_one_def() {
184		let g = build_from_json(java_minimal()).unwrap();
185		assert_eq!(g.def_count(), 2);
186		assert_eq!(g.ref_count(), 0);
187	}
188
189	#[test]
190	fn every_declared_def_has_origin_declared() {
191		let g = build_from_json(java_minimal()).unwrap();
192		let class_def = g.defs().nth(1).unwrap();
193		assert_eq!(class_def.origin, ORIGIN_DECLARED.to_vec());
194	}
195
196	#[test]
197	fn root_def_keeps_origin_extracted_for_now() {
198		let g = build_from_json(java_minimal()).unwrap();
199		let root_def = g.defs().next().unwrap();
200		assert_eq!(root_def.origin, ORIGIN_EXTRACTED.to_vec());
201	}
202
203	#[test]
204	fn rejects_kind_mismatch_with_moniker_last_segment() {
205		let v = json!({
206			"root": "code+moniker://app/srcset:main/lang:java/package:com/module:Foo",
207			"lang": "java",
208			"symbols": [{
209				"moniker": "code+moniker://app/srcset:main/lang:java/package:com/module:Foo/class:Foo",
210				"kind": "interface",
211				"parent": "code+moniker://app/srcset:main/lang:java/package:com/module:Foo"
212			}]
213		});
214		let err = build_from_json(v).unwrap_err();
215		assert!(matches!(
216			err,
217			DeclareError::KindMismatchMoniker { ref declared_kind, ref moniker_last_kind, .. }
218				if declared_kind == "interface" && moniker_last_kind == "class"
219		));
220	}
221
222	#[test]
223	fn rejects_unknown_parent() {
224		let v = json!({
225			"root": "code+moniker://app/srcset:main/lang:java/package:com/module:Foo",
226			"lang": "java",
227			"symbols": [{
228				"moniker": "code+moniker://app/srcset:main/lang:java/package:com/module:Foo/class:Foo/method:bar()",
229				"kind": "method",
230				"parent": "code+moniker://app/srcset:main/lang:java/package:com/module:Foo/class:DoesNotExist"
231			}]
232		});
233		let err = build_from_json(v).unwrap_err();
234		assert!(matches!(err, DeclareError::UnknownParent { .. }));
235	}
236
237	#[test]
238	fn rejects_duplicate_moniker_in_symbols() {
239		let v = json!({
240			"root": "code+moniker://app/srcset:main/lang:java/package:com/module:Foo",
241			"lang": "java",
242			"symbols": [
243				{
244					"moniker": "code+moniker://app/srcset:main/lang:java/package:com/module:Foo/class:Foo",
245					"kind": "class",
246					"parent": "code+moniker://app/srcset:main/lang:java/package:com/module:Foo"
247				},
248				{
249					"moniker": "code+moniker://app/srcset:main/lang:java/package:com/module:Foo/class:Foo",
250					"kind": "class",
251					"parent": "code+moniker://app/srcset:main/lang:java/package:com/module:Foo"
252				}
253			]
254		});
255		let err = build_from_json(v).unwrap_err();
256		assert!(matches!(err, DeclareError::DuplicateMoniker { .. }));
257	}
258
259	#[test]
260	fn out_of_order_symbols_are_topologically_sorted() {
261		let v = json!({
262			"root": "code+moniker://app/srcset:main/lang:java/package:com/module:Foo",
263			"lang": "java",
264			"symbols": [
265				{
266					"moniker": "code+moniker://app/srcset:main/lang:java/package:com/module:Foo/class:Foo/method:bar()",
267					"kind": "method",
268					"parent": "code+moniker://app/srcset:main/lang:java/package:com/module:Foo/class:Foo"
269				},
270				{
271					"moniker": "code+moniker://app/srcset:main/lang:java/package:com/module:Foo/class:Foo",
272					"kind": "class",
273					"parent": "code+moniker://app/srcset:main/lang:java/package:com/module:Foo"
274				}
275			]
276		});
277		let g = build_from_json(v).unwrap();
278		assert_eq!(g.def_count(), 3);
279	}
280
281	#[test]
282	fn calls_intra_module_get_local_binding() {
283		let v = json!({
284			"root": "code+moniker://app/srcset:main/lang:rs/module:svc",
285			"lang": "rs",
286			"symbols": [
287				{
288					"moniker": "code+moniker://app/srcset:main/lang:rs/module:svc/fn:f()",
289					"kind": "fn",
290					"parent": "code+moniker://app/srcset:main/lang:rs/module:svc"
291				},
292				{
293					"moniker": "code+moniker://app/srcset:main/lang:rs/module:svc/fn:g()",
294					"kind": "fn",
295					"parent": "code+moniker://app/srcset:main/lang:rs/module:svc"
296				}
297			],
298			"edges": [{
299				"from": "code+moniker://app/srcset:main/lang:rs/module:svc/fn:f()",
300				"kind": "calls",
301				"to":   "code+moniker://app/srcset:main/lang:rs/module:svc/fn:g()"
302			}]
303		});
304		let g = build_from_json(v).unwrap();
305		let r = g.refs().next().unwrap();
306		assert_eq!(r.binding, b"local".to_vec());
307	}
308
309	#[test]
310	fn calls_cross_module_get_none_binding() {
311		let v = json!({
312			"root": "code+moniker://app/srcset:main/lang:rs/module:svc",
313			"lang": "rs",
314			"symbols": [{
315				"moniker": "code+moniker://app/srcset:main/lang:rs/module:svc/fn:f()",
316				"kind": "fn",
317				"parent": "code+moniker://app/srcset:main/lang:rs/module:svc"
318			}],
319			"edges": [{
320				"from": "code+moniker://app/srcset:main/lang:rs/module:svc/fn:f()",
321				"kind": "calls",
322				"to":   "code+moniker://app/srcset:main/lang:rs/module:other/fn:g()"
323			}]
324		});
325		let g = build_from_json(v).unwrap();
326		let r = g.refs().next().unwrap();
327		assert_eq!(r.binding, b"none".to_vec());
328	}
329
330	#[test]
331	fn depends_on_lowers_to_imports_module_with_import_binding() {
332		let v = json!({
333			"root": "code+moniker://app/srcset:main/lang:rs/module:svc",
334			"lang": "rs",
335			"symbols": [{
336				"moniker": "code+moniker://app/srcset:main/lang:rs/module:svc/fn:f()",
337				"kind": "fn",
338				"parent": "code+moniker://app/srcset:main/lang:rs/module:svc"
339			}],
340			"edges": [{
341				"from": "code+moniker://app/srcset:main/lang:rs/module:svc/fn:f()",
342				"kind": "depends_on",
343				"to":   "code+moniker://app/external_pkg:cargo/path:serde"
344			}]
345		});
346		let g = build_from_json(v).unwrap();
347		let r = g.refs().next().unwrap();
348		assert_eq!(r.kind, b"imports_module".to_vec());
349		assert_eq!(r.binding, b"import".to_vec());
350	}
351
352	#[test]
353	fn injects_provide_lowers_to_di_register_with_inject_binding() {
354		let v = json!({
355			"root": "code+moniker://app/srcset:main/lang:rs/module:svc",
356			"lang": "rs",
357			"symbols": [{
358				"moniker": "code+moniker://app/srcset:main/lang:rs/module:svc/fn:f()",
359				"kind": "fn",
360				"parent": "code+moniker://app/srcset:main/lang:rs/module:svc"
361			}],
362			"edges": [{
363				"from": "code+moniker://app/srcset:main/lang:rs/module:svc/fn:f()",
364				"kind": "injects:provide",
365				"to":   "code+moniker://app/srcset:main/lang:rs/module:other/trait:T"
366			}]
367		});
368		let g = build_from_json(v).unwrap();
369		let r = g.refs().next().unwrap();
370		assert_eq!(r.kind, b"di_register".to_vec());
371		assert_eq!(r.binding, b"inject".to_vec());
372	}
373
374	#[test]
375	fn injects_require_lowers_to_di_require_with_inject_binding() {
376		let v = json!({
377			"root": "code+moniker://app/srcset:main/lang:rs/module:svc",
378			"lang": "rs",
379			"symbols": [{
380				"moniker": "code+moniker://app/srcset:main/lang:rs/module:svc/fn:f()",
381				"kind": "fn",
382				"parent": "code+moniker://app/srcset:main/lang:rs/module:svc"
383			}],
384			"edges": [{
385				"from": "code+moniker://app/srcset:main/lang:rs/module:svc/fn:f()",
386				"kind": "injects:require",
387				"to":   "code+moniker://app/srcset:main/lang:rs/module:other/trait:U"
388			}]
389		});
390		let g = build_from_json(v).unwrap();
391		let r = g.refs().next().unwrap();
392		assert_eq!(r.kind, b"di_require".to_vec());
393		assert_eq!(r.binding, b"inject".to_vec());
394	}
395
396	#[test]
397	fn rejects_edge_from_undeclared_symbol() {
398		let v = json!({
399			"root": "code+moniker://app/srcset:main/lang:rs/module:svc",
400			"lang": "rs",
401			"symbols": [],
402			"edges": [{
403				"from": "code+moniker://app/srcset:main/lang:rs/module:svc/fn:undeclared()",
404				"kind": "calls",
405				"to":   "code+moniker://app/srcset:main/lang:rs/module:other/fn:g()"
406			}]
407		});
408		let err = build_from_json(v).unwrap_err();
409		assert!(matches!(err, DeclareError::UnknownEdgeSource { .. }));
410	}
411
412	#[test]
413	fn edge_to_unknown_target_is_accepted() {
414		let v = json!({
415			"root": "code+moniker://app/srcset:main/lang:rs/module:svc",
416			"lang": "rs",
417			"symbols": [{
418				"moniker": "code+moniker://app/srcset:main/lang:rs/module:svc/fn:f()",
419				"kind": "fn",
420				"parent": "code+moniker://app/srcset:main/lang:rs/module:svc"
421			}],
422			"edges": [{
423				"from": "code+moniker://app/srcset:main/lang:rs/module:svc/fn:f()",
424				"kind": "calls",
425				"to":   "code+moniker://app/srcset:main/lang:rs/module:never_extracted/fn:phantom()"
426			}]
427		});
428		assert!(build_from_json(v).is_ok());
429	}
430
431	#[test]
432	fn shares_module_handles_nested_class_in_module() {
433		let svc_f =
434			parse_uri("code+moniker://app/srcset:main/lang:rs/module:svc/class:C/method:f()");
435		let svc_g =
436			parse_uri("code+moniker://app/srcset:main/lang:rs/module:svc/class:C/method:g()");
437		assert!(shares_module(&svc_f, &svc_g));
438	}
439
440	#[test]
441	fn shares_module_returns_false_when_no_module_segment() {
442		let java_a =
443			parse_uri("code+moniker://app/srcset:main/lang:java/package:com/class:A/method:f()");
444		let java_b =
445			parse_uri("code+moniker://app/srcset:main/lang:java/package:com/class:A/method:g()");
446		assert!(!shares_module(&java_a, &java_b));
447	}
448
449	#[test]
450	fn declared_def_bind_matches_extracted_def_with_same_moniker() {
451		let m1 = MonikerBuilder::new()
452			.project(b"app")
453			.segment(b"srcset", b"main")
454			.segment(b"lang", b"java")
455			.segment(b"package", b"com")
456			.segment(b"module", b"Foo")
457			.segment(b"class", b"Foo")
458			.build();
459		let m2 = MonikerBuilder::new()
460			.project(b"app")
461			.segment(b"srcset", b"main")
462			.segment(b"lang", b"java")
463			.segment(b"package", b"com")
464			.segment(b"module", b"Foo")
465			.segment(b"class", b"Foo")
466			.build();
467		assert!(m1.bind_match(&m2));
468	}
469}