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}