1use std::collections::HashMap;
26use std::path::{Path, PathBuf};
27
28use log::debug;
29use sqry_core::graph::node::Language;
30use sqry_core::graph::unified::build::StagingGraph;
31use sqry_core::graph::unified::concurrent::CodeGraph;
32use sqry_core::graph::unified::edge::EdgeKind;
33use sqry_core::graph::unified::node::NodeKind;
34use sqry_core::graph::unified::storage::metadata::{
35 ClasspathNodeMetadata, NodeMetadataStore, TypedMetadata,
36};
37use sqry_core::graph::unified::storage::registry::FileRegistry;
38use sqry_core::graph::unified::storage::{NodeEntry, StringInterner};
39use sqry_core::graph::unified::{FileId, NodeId, StringId};
40
41use crate::stub::index::ClasspathIndex;
42use crate::stub::model::{ClassKind, ClassStub};
43use crate::{ClasspathError, ClasspathResult};
44
45use super::provenance::ClasspathProvenance;
46
47struct InternHelper<'a> {
58 interner: &'a mut StringInterner,
59 cache: HashMap<String, StringId>,
60}
61
62impl<'a> InternHelper<'a> {
63 fn new(interner: &'a mut StringInterner) -> Self {
64 Self {
65 interner,
66 cache: HashMap::new(),
67 }
68 }
69
70 fn intern(&mut self, s: &str) -> ClasspathResult<StringId> {
75 if let Some(&id) = self.cache.get(s) {
76 return Ok(id);
77 }
78 let id = self.interner.intern(s).map_err(|e| {
79 ClasspathError::EmissionError(format!("string intern failed for '{s}': {e}"))
80 })?;
81 self.cache.insert(s.to_owned(), id);
82 Ok(id)
83 }
84}
85
86#[allow(clippy::trivially_copy_pass_by_ref)] fn access_to_visibility(access: &crate::stub::model::AccessFlags) -> &'static str {
93 if access.is_public() {
94 "public"
95 } else if access.is_protected() {
96 "protected"
97 } else if access.is_private() {
98 "private"
99 } else {
100 "package"
101 }
102}
103
104fn class_kind_to_node_kind(kind: ClassKind) -> NodeKind {
106 match kind {
107 ClassKind::Class | ClassKind::Record => NodeKind::Class,
108 ClassKind::Interface => NodeKind::Interface,
109 ClassKind::Enum => NodeKind::Enum,
110 ClassKind::Annotation => NodeKind::Annotation,
111 ClassKind::Module => NodeKind::JavaModule,
112 }
113}
114
115fn register_synthetic_file(
124 jar_path: &Path,
125 fqn: &str,
126 file_registry: &mut FileRegistry,
127) -> ClasspathResult<FileId> {
128 let class_path_str = fqn.replace('.', "/");
129 let synthetic_path = format!("{}!/{class_path_str}.class", jar_path.display());
130 let path = PathBuf::from(&synthetic_path);
131 file_registry
132 .register_external(&path, Some(Language::Java))
133 .map_err(|e| {
134 ClasspathError::EmissionError(format!(
135 "failed to register synthetic file for {fqn}: {e}"
136 ))
137 })
138}
139
140#[allow(clippy::too_many_lines)]
162pub fn emit_classpath_nodes(
163 index: &ClasspathIndex,
164 staging: &mut StagingGraph,
165 file_registry: &mut FileRegistry,
166 interner: &mut StringInterner,
167 metadata_store: &mut NodeMetadataStore,
168 provenance: &[ClasspathProvenance],
169) -> ClasspathResult<EmissionResult> {
170 let mut helper = InternHelper::new(interner);
171 let mut fqn_to_node: HashMap<String, NodeId> = HashMap::with_capacity(index.classes.len());
172 let mut fqn_to_nodes: HashMap<String, Vec<ClasspathNodeRef>> =
173 HashMap::with_capacity(index.classes.len());
174 let mut file_id_map: HashMap<String, FileId> = HashMap::new();
175
176 let prov_map: HashMap<&Path, &ClasspathProvenance> = provenance
178 .iter()
179 .map(|p| (p.jar_path.as_path(), p))
180 .collect();
181
182 for stub in &index.classes {
183 emit_class_stub(
184 stub,
185 staging,
186 file_registry,
187 &mut helper,
188 metadata_store,
189 &prov_map,
190 &mut fqn_to_node,
191 &mut fqn_to_nodes,
192 &mut file_id_map,
193 )?;
194 }
195
196 Ok(EmissionResult {
197 fqn_to_node,
198 fqn_to_nodes,
199 file_id_map,
200 })
201}
202
203#[derive(Debug, Clone)]
205pub struct ClasspathNodeRef {
206 pub node_id: NodeId,
208 pub fqn: String,
210 pub jar_path: PathBuf,
212 pub file_id: FileId,
214}
215
216#[derive(Debug)]
218pub struct EmissionResult {
219 pub fqn_to_node: HashMap<String, NodeId>,
221 pub fqn_to_nodes: HashMap<String, Vec<ClasspathNodeRef>>,
223 pub file_id_map: HashMap<String, FileId>,
225}
226
227pub fn emit_into_code_graph(
239 index: &ClasspathIndex,
240 graph: &mut CodeGraph,
241 provenance: &[ClasspathProvenance],
242) -> ClasspathResult<EmissionResult> {
243 let mut nodes = graph.nodes().clone();
246 let edges = graph.edges().clone();
247 let mut strings = graph.strings().clone();
248 let mut files = graph.files().clone();
249 let mut metadata = graph.macro_metadata().clone();
250
251 let mut staging = StagingGraph::new();
252 let emission_result = emit_classpath_nodes(
253 index,
254 &mut staging,
255 &mut files,
256 &mut strings,
257 &mut metadata,
258 provenance,
259 )?;
260
261 create_classpath_edges(
262 index,
263 &emission_result.fqn_to_nodes,
264 provenance,
265 &mut staging,
266 );
267
268 let id_mapping = staging
269 .commit_nodes(&mut nodes)
270 .map_err(|e| ClasspathError::EmissionError(format!("node commit failed: {e}")))?;
271
272 for edge in staging.get_remapped_edges(&id_mapping) {
273 let _delta = edges.add_edge(edge.source, edge.target, edge.kind, edge.file);
274 }
275
276 let mut remapped_fqn_to_node = HashMap::with_capacity(emission_result.fqn_to_node.len());
277 for (fqn, node_id) in emission_result.fqn_to_node {
278 let remapped_id = id_mapping.get(&node_id).copied().unwrap_or(node_id);
279 remapped_fqn_to_node.insert(fqn, remapped_id);
280 }
281
282 let mut remapped_fqn_to_nodes = HashMap::with_capacity(emission_result.fqn_to_nodes.len());
283 for (fqn, refs) in emission_result.fqn_to_nodes {
284 let remapped_refs = refs
285 .into_iter()
286 .map(|node_ref| ClasspathNodeRef {
287 node_id: id_mapping
288 .get(&node_ref.node_id)
289 .copied()
290 .unwrap_or(node_ref.node_id),
291 fqn: node_ref.fqn,
292 jar_path: node_ref.jar_path,
293 file_id: node_ref.file_id,
294 })
295 .collect();
296 remapped_fqn_to_nodes.insert(fqn, remapped_refs);
297 }
298
299 if !id_mapping.is_empty() {
300 let remapped_entries: Vec<_> = metadata
301 .iter_entries()
302 .map(|((index, generation), entry)| {
303 let node_id = NodeId::new(index, generation);
304 let remapped_id = id_mapping.get(&node_id).copied().unwrap_or(node_id);
305 (remapped_id, entry.clone())
306 })
307 .collect();
308 metadata = NodeMetadataStore::new();
309 for (node_id, entry) in remapped_entries {
310 metadata.insert_entry(node_id, entry);
311 }
312 }
313
314 let _old_nodes = std::mem::replace(graph.nodes_mut(), nodes);
315 let _old_edges = std::mem::replace(graph.edges_mut(), edges);
316 let _old_strings = std::mem::replace(graph.strings_mut(), strings);
317 let _old_files = std::mem::replace(graph.files_mut(), files);
318 let _old_metadata = std::mem::replace(graph.macro_metadata_mut(), metadata);
319
320 Ok(EmissionResult {
321 fqn_to_node: remapped_fqn_to_node,
322 fqn_to_nodes: remapped_fqn_to_nodes,
323 file_id_map: emission_result.file_id_map,
324 })
325}
326
327#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_lines)]
330#[allow(clippy::similar_names)] fn emit_class_stub(
332 stub: &ClassStub,
333 staging: &mut StagingGraph,
334 file_registry: &mut FileRegistry,
335 helper: &mut InternHelper<'_>,
336 metadata_store: &mut NodeMetadataStore,
337 prov_map: &HashMap<&Path, &ClasspathProvenance>,
338 fqn_to_node: &mut HashMap<String, NodeId>,
339 fqn_to_nodes: &mut HashMap<String, Vec<ClasspathNodeRef>>,
340 file_id_map: &mut HashMap<String, FileId>,
341) -> ClasspathResult<()> {
342 let jar_path = if let Some(ref src_jar) = stub.source_jar {
345 PathBuf::from(src_jar)
346 } else if let Some((&path, _)) = prov_map.iter().next() {
347 path.to_path_buf()
348 } else {
349 PathBuf::from(format!("<classpath>/{}.class", stub.fqn.replace('.', "/")))
350 };
351 let file_id = register_synthetic_file(&jar_path, &stub.fqn, file_registry)?;
352 file_id_map.insert(stub.fqn.clone(), file_id);
353
354 let node_kind = class_kind_to_node_kind(stub.kind);
356 let name_id = helper.intern(&stub.name)?;
357 let qname_id = helper.intern(&stub.fqn)?;
358 let vis_id = helper.intern(access_to_visibility(&stub.access))?;
359
360 let class_entry = NodeEntry::new(node_kind, name_id, file_id)
361 .with_qualified_name(qname_id)
362 .with_visibility(vis_id)
363 .with_static(stub.access.is_static())
364 .with_unsafe(false);
365
366 let class_node_id = staging.add_node(class_entry);
367 record_node_ref(
368 &stub.fqn,
369 class_node_id,
370 &jar_path,
371 file_id,
372 fqn_to_node,
373 fqn_to_nodes,
374 );
375
376 let prov = find_provenance_for_jar(&jar_path, prov_map);
378 let cp_meta = ClasspathNodeMetadata {
379 coordinates: prov.and_then(|p| p.coordinates.clone()),
380 jar_path: jar_path.display().to_string(),
381 fqn: stub.fqn.clone(),
382 is_direct_dependency: prov.is_some_and(ClasspathProvenance::has_direct_scope),
383 };
384 metadata_store.insert_typed(class_node_id, TypedMetadata::Classpath(cp_meta.clone()));
385
386 for method in &stub.methods {
388 let method_name_id = helper.intern(&method.name)?;
389 let method_fqn = format!("{}.{}{}", stub.fqn, method.name, method.descriptor);
392 #[allow(clippy::similar_names)] let method_qname_id = helper.intern(&method_fqn)?;
394 let method_vis_id = helper.intern(access_to_visibility(&method.access))?;
395
396 let method_entry = NodeEntry::new(NodeKind::Method, method_name_id, file_id)
397 .with_qualified_name(method_qname_id)
398 .with_visibility(method_vis_id)
399 .with_static(method.access.is_static());
400
401 let method_node_id = staging.add_node(method_entry);
402 record_node_ref(
403 &method_fqn,
404 method_node_id,
405 &jar_path,
406 file_id,
407 fqn_to_node,
408 fqn_to_nodes,
409 );
410 metadata_store.insert_typed(method_node_id, TypedMetadata::Classpath(cp_meta.clone()));
411
412 staging.add_edge(class_node_id, method_node_id, EdgeKind::Defines, file_id);
414 }
415
416 for field in &stub.fields {
418 let field_name_id = helper.intern(&field.name)?;
419 let field_fqn = format!("{}.{}", stub.fqn, field.name);
420 let field_qname_id = helper.intern(&field_fqn)?;
421 let field_vis_id = helper.intern(access_to_visibility(&field.access))?;
422
423 let field_entry = NodeEntry::new(NodeKind::Property, field_name_id, file_id)
424 .with_qualified_name(field_qname_id)
425 .with_visibility(field_vis_id)
426 .with_static(field.access.is_static());
427
428 let field_node_id = staging.add_node(field_entry);
429 record_node_ref(
430 &field_fqn,
431 field_node_id,
432 &jar_path,
433 file_id,
434 fqn_to_node,
435 fqn_to_nodes,
436 );
437 metadata_store.insert_typed(field_node_id, TypedMetadata::Classpath(cp_meta.clone()));
438
439 staging.add_edge(class_node_id, field_node_id, EdgeKind::Defines, file_id);
441 }
442
443 for constant_name in &stub.enum_constants {
445 let const_name_id = helper.intern(constant_name)?;
446 let const_fqn = format!("{}.{constant_name}", stub.fqn);
447 let const_qname_id = helper.intern(&const_fqn)?;
448
449 let const_entry = NodeEntry::new(NodeKind::EnumConstant, const_name_id, file_id)
450 .with_qualified_name(const_qname_id)
451 .with_visibility(helper.intern("public")?);
452
453 let const_node_id = staging.add_node(const_entry);
454 record_node_ref(
455 &const_fqn,
456 const_node_id,
457 &jar_path,
458 file_id,
459 fqn_to_node,
460 fqn_to_nodes,
461 );
462 metadata_store.insert_typed(const_node_id, TypedMetadata::Classpath(cp_meta.clone()));
463
464 staging.add_edge(class_node_id, const_node_id, EdgeKind::Defines, file_id);
466 }
467
468 if let Some(ref gen_sig) = stub.generic_signature {
470 for tp in &gen_sig.type_parameters {
471 let tp_name_id = helper.intern(&tp.name)?;
472 let tp_fqn = format!("{}.<{}>", stub.fqn, tp.name);
473 let tp_qname_id = helper.intern(&tp_fqn)?;
474
475 let tp_entry = NodeEntry::new(NodeKind::TypeParameter, tp_name_id, file_id)
476 .with_qualified_name(tp_qname_id);
477
478 let tp_node_id = staging.add_node(tp_entry);
479 record_node_ref(
480 &tp_fqn,
481 tp_node_id,
482 &jar_path,
483 file_id,
484 fqn_to_node,
485 fqn_to_nodes,
486 );
487 metadata_store.insert_typed(tp_node_id, TypedMetadata::Classpath(cp_meta.clone()));
488
489 staging.add_edge(class_node_id, tp_node_id, EdgeKind::TypeArgument, file_id);
491 }
492 }
493
494 for lambda in &stub.lambda_targets {
496 let lambda_label = format!("{}::{}", lambda.owner_fqn, lambda.method_name);
497 let lambda_name_id = helper.intern(&lambda_label)?;
498 let lambda_fqn = format!("{}.lambda${}", stub.fqn, lambda.method_name);
499 let lambda_qname_id = helper.intern(&lambda_fqn)?;
500
501 let lambda_entry = NodeEntry::new(NodeKind::LambdaTarget, lambda_name_id, file_id)
502 .with_qualified_name(lambda_qname_id);
503
504 let lambda_node_id = staging.add_node(lambda_entry);
505 record_node_ref(
506 &lambda_fqn,
507 lambda_node_id,
508 &jar_path,
509 file_id,
510 fqn_to_node,
511 fqn_to_nodes,
512 );
513 metadata_store.insert_typed(lambda_node_id, TypedMetadata::Classpath(cp_meta.clone()));
514
515 staging.add_edge(class_node_id, lambda_node_id, EdgeKind::Contains, file_id);
517 }
518
519 for inner in &stub.inner_classes {
521 if inner.outer_fqn.as_deref() == Some(&stub.fqn) {
524 }
530 }
531
532 if let Some(ref module) = stub.module {
534 let mod_name_id = helper.intern(&module.name)?;
535 let mod_fqn = format!("module:{}", module.name);
536 let mod_qname_id = helper.intern(&mod_fqn)?;
537
538 let mod_entry = NodeEntry::new(NodeKind::JavaModule, mod_name_id, file_id)
539 .with_qualified_name(mod_qname_id);
540
541 let mod_node_id = staging.add_node(mod_entry);
542 record_node_ref(
543 &mod_fqn,
544 mod_node_id,
545 &jar_path,
546 file_id,
547 fqn_to_node,
548 fqn_to_nodes,
549 );
550 metadata_store.insert_typed(mod_node_id, TypedMetadata::Classpath(cp_meta.clone()));
551
552 staging.add_edge(class_node_id, mod_node_id, EdgeKind::Contains, file_id);
554 }
555
556 Ok(())
557}
558
559fn record_node_ref(
560 fqn: &str,
561 node_id: NodeId,
562 jar_path: &Path,
563 file_id: FileId,
564 fqn_to_node: &mut HashMap<String, NodeId>,
565 fqn_to_nodes: &mut HashMap<String, Vec<ClasspathNodeRef>>,
566) {
567 fqn_to_node.entry(fqn.to_owned()).or_insert(node_id);
568 fqn_to_nodes
569 .entry(fqn.to_owned())
570 .or_default()
571 .push(ClasspathNodeRef {
572 node_id,
573 fqn: fqn.to_owned(),
574 jar_path: jar_path.to_path_buf(),
575 file_id,
576 });
577}
578
579fn find_provenance_for_jar<'a>(
585 jar_path: &Path,
586 prov_map: &HashMap<&Path, &'a ClasspathProvenance>,
587) -> Option<&'a ClasspathProvenance> {
588 prov_map.get(jar_path).copied()
589}
590
591#[allow(clippy::too_many_lines)]
602#[allow(clippy::implicit_hasher)] pub fn create_classpath_edges(
604 #[allow(clippy::implicit_hasher)] index: &ClasspathIndex,
606 fqn_to_nodes: &HashMap<String, Vec<ClasspathNodeRef>>,
607 provenance: &[ClasspathProvenance],
608 staging: &mut StagingGraph,
609) {
610 let prov_map: HashMap<&Path, &ClasspathProvenance> = provenance
611 .iter()
612 .map(|p| (p.jar_path.as_path(), p))
613 .collect();
614
615 for stub in &index.classes {
616 let source_jar = stub.source_jar.as_deref().map(Path::new);
617 let Some(class_node) = select_node_ref(&stub.fqn, source_jar, fqn_to_nodes, &prov_map)
618 else {
619 continue;
620 };
621 let class_node_id = class_node.node_id;
622 let file_id = class_node.file_id;
623
624 if let Some(ref superclass_fqn) = stub.superclass
626 && superclass_fqn != "java.lang.Object"
627 {
628 if let Some(super_node) =
629 select_node_ref(superclass_fqn, source_jar, fqn_to_nodes, &prov_map)
630 {
631 let super_node_id = super_node.node_id;
632 staging.add_edge(class_node_id, super_node_id, EdgeKind::Inherits, file_id);
633 } else {
634 log::debug!(
635 "classpath: skipping Inherits edge for {} → {} (target not in graph)",
636 stub.fqn,
637 superclass_fqn
638 );
639 }
640 }
641
642 for iface_fqn in &stub.interfaces {
644 if let Some(iface_node) =
645 select_node_ref(iface_fqn, source_jar, fqn_to_nodes, &prov_map)
646 {
647 let iface_node_id = iface_node.node_id;
648 staging.add_edge(class_node_id, iface_node_id, EdgeKind::Implements, file_id);
649 } else {
650 log::debug!(
651 "classpath: skipping Implements edge for {} → {} (target not in graph)",
652 stub.fqn,
653 iface_fqn
654 );
655 }
656 }
657
658 if let Some(ref gen_sig) = stub.generic_signature {
660 for tp in &gen_sig.type_parameters {
661 let tp_fqn = format!("{}.<{}>", stub.fqn, tp.name);
662 let Some(tp_node) = select_node_ref(&tp_fqn, source_jar, fqn_to_nodes, &prov_map)
663 else {
664 continue;
665 };
666 let tp_node_id = tp_node.node_id;
667
668 if let Some(ref bound) = tp.class_bound
670 && let Some(bound_fqn) = extract_class_fqn_from_type_sig(bound)
671 && let Some(bound_node) =
672 select_node_ref(bound_fqn, source_jar, fqn_to_nodes, &prov_map)
673 {
674 let bound_node_id = bound_node.node_id;
675 staging.add_edge(tp_node_id, bound_node_id, EdgeKind::GenericBound, file_id);
676 }
677
678 for ibound in &tp.interface_bounds {
680 if let Some(bound_fqn) = extract_class_fqn_from_type_sig(ibound)
681 && let Some(bound_node) =
682 select_node_ref(bound_fqn, source_jar, fqn_to_nodes, &prov_map)
683 {
684 let bound_node_id = bound_node.node_id;
685 staging.add_edge(
686 tp_node_id,
687 bound_node_id,
688 EdgeKind::GenericBound,
689 file_id,
690 );
691 }
692 }
693 }
694 }
695
696 for ann in &stub.annotations {
698 if let Some(ann_type_node) =
699 select_node_ref(&ann.type_fqn, source_jar, fqn_to_nodes, &prov_map)
700 {
701 let ann_type_node_id = ann_type_node.node_id;
702 staging.add_edge(
703 class_node_id,
704 ann_type_node_id,
705 EdgeKind::AnnotatedWith,
706 file_id,
707 );
708 }
709 }
710
711 for method in &stub.methods {
713 let method_fqn = format!("{}.{}{}", stub.fqn, method.name, method.descriptor);
714 if let Some(method_node) =
715 select_node_ref(&method_fqn, source_jar, fqn_to_nodes, &prov_map)
716 {
717 let method_node_id = method_node.node_id;
718 for ann in &method.annotations {
719 if let Some(ann_type_node) =
720 select_node_ref(&ann.type_fqn, source_jar, fqn_to_nodes, &prov_map)
721 {
722 let ann_type_node_id = ann_type_node.node_id;
723 staging.add_edge(
724 method_node_id,
725 ann_type_node_id,
726 EdgeKind::AnnotatedWith,
727 file_id,
728 );
729 }
730 }
731 }
732 }
733
734 for field in &stub.fields {
736 let field_fqn = format!("{}.{}", stub.fqn, field.name);
737 if let Some(field_node) =
738 select_node_ref(&field_fqn, source_jar, fqn_to_nodes, &prov_map)
739 {
740 let field_node_id = field_node.node_id;
741 for ann in &field.annotations {
742 if let Some(ann_type_node) =
743 select_node_ref(&ann.type_fqn, source_jar, fqn_to_nodes, &prov_map)
744 {
745 let ann_type_node_id = ann_type_node.node_id;
746 staging.add_edge(
747 field_node_id,
748 ann_type_node_id,
749 EdgeKind::AnnotatedWith,
750 file_id,
751 );
752 }
753 }
754 }
755 }
756
757 for inner in &stub.inner_classes {
759 if inner.outer_fqn.as_deref() == Some(&stub.fqn)
760 && let Some(inner_node) =
761 select_node_ref(&inner.inner_fqn, source_jar, fqn_to_nodes, &prov_map)
762 {
763 let inner_node_id = inner_node.node_id;
764 staging.add_edge(class_node_id, inner_node_id, EdgeKind::Contains, file_id);
765 }
766 }
767
768 if let Some(ref module) = stub.module {
770 let mod_fqn = format!("module:{}", module.name);
771 let Some(mod_node) = select_node_ref(&mod_fqn, source_jar, fqn_to_nodes, &prov_map)
772 else {
773 continue;
774 };
775 let mod_node_id = mod_node.node_id;
776
777 for req in &module.requires {
779 let req_mod_fqn = format!("module:{}", req.module_name);
780 if let Some(req_node) =
781 select_node_ref(&req_mod_fqn, source_jar, fqn_to_nodes, &prov_map)
782 {
783 let req_node_id = req_node.node_id;
784 staging.add_edge(mod_node_id, req_node_id, EdgeKind::ModuleRequires, file_id);
785 }
786 }
787
788 for exp in &module.exports {
790 let pkg_classes = index.lookup_package(&exp.package);
792 for pkg_class in pkg_classes {
793 for pkg_class_node in
794 select_node_refs(&pkg_class.fqn, source_jar, fqn_to_nodes, &prov_map)
795 {
796 let pkg_class_node_id = pkg_class_node.node_id;
797 staging.add_edge(
798 mod_node_id,
799 pkg_class_node_id,
800 EdgeKind::ModuleExports,
801 file_id,
802 );
803 }
804 }
805 }
806
807 for opens in &module.opens {
809 let pkg_classes = index.lookup_package(&opens.package);
810 for pkg_class in pkg_classes {
811 for pkg_class_node in
812 select_node_refs(&pkg_class.fqn, source_jar, fqn_to_nodes, &prov_map)
813 {
814 let pkg_class_node_id = pkg_class_node.node_id;
815 staging.add_edge(
816 mod_node_id,
817 pkg_class_node_id,
818 EdgeKind::ModuleOpens,
819 file_id,
820 );
821 }
822 }
823 }
824
825 for provides in &module.provides {
827 for impl_fqn in &provides.implementations {
828 if let Some(impl_node) =
829 select_node_ref(impl_fqn, source_jar, fqn_to_nodes, &prov_map)
830 {
831 let impl_node_id = impl_node.node_id;
832 staging.add_edge(
833 mod_node_id,
834 impl_node_id,
835 EdgeKind::ModuleProvides,
836 file_id,
837 );
838 }
839 }
840 }
841 }
842 }
843}
844
845fn select_node_ref<'a>(
846 fqn: &str,
847 source_jar: Option<&Path>,
848 fqn_to_nodes: &'a HashMap<String, Vec<ClasspathNodeRef>>,
849 prov_map: &HashMap<&Path, &ClasspathProvenance>,
850) -> Option<&'a ClasspathNodeRef> {
851 let candidates = select_node_refs(fqn, source_jar, fqn_to_nodes, prov_map);
852 candidates.first().copied()
853}
854
855fn select_node_refs<'a>(
856 fqn: &str,
857 source_jar: Option<&Path>,
858 fqn_to_nodes: &'a HashMap<String, Vec<ClasspathNodeRef>>,
859 prov_map: &HashMap<&Path, &ClasspathProvenance>,
860) -> Vec<&'a ClasspathNodeRef> {
861 let Some(candidates) = fqn_to_nodes.get(fqn) else {
862 return Vec::new();
863 };
864
865 let Some(source_jar) = source_jar else {
866 return candidates.iter().collect();
867 };
868
869 if let Some(exact_match) = candidates
870 .iter()
871 .find(|candidate| candidate.jar_path.as_path() == source_jar)
872 {
873 return vec![exact_match];
874 }
875
876 let scoped: Vec<_> = candidates
877 .iter()
878 .filter(|candidate| jars_share_scope(source_jar, candidate.jar_path.as_path(), prov_map))
879 .collect();
880 if scoped.is_empty() {
881 candidates.iter().collect()
882 } else {
883 scoped
884 }
885}
886
887fn jars_share_scope(
888 source_jar: &Path,
889 target_jar: &Path,
890 prov_map: &HashMap<&Path, &ClasspathProvenance>,
891) -> bool {
892 if source_jar == target_jar {
893 return true;
894 }
895
896 let Some(source) = prov_map.get(source_jar) else {
897 debug!(
898 "classpath: provenance missing for source jar {}; allowing scope fallback",
899 source_jar.display()
900 );
901 return true;
902 };
903 let Some(target) = prov_map.get(target_jar) else {
904 debug!(
905 "classpath: provenance missing for target jar {}; allowing scope fallback",
906 target_jar.display()
907 );
908 return true;
909 };
910
911 source.scopes.iter().any(|source_scope| {
912 target
913 .scopes
914 .iter()
915 .any(|target_scope| source_scope.module_root == target_scope.module_root)
916 })
917}
918
919fn extract_class_fqn_from_type_sig(sig: &crate::stub::model::TypeSignature) -> Option<&str> {
921 match sig {
922 crate::stub::model::TypeSignature::Class { fqn, .. } => Some(fqn.as_str()),
923 _ => None,
924 }
925}
926
927#[cfg(test)]
932mod tests {
933 use super::*;
934 use crate::stub::model::{
935 AccessFlags, AnnotationStub, ClassKind, FieldStub, GenericClassSignature, InnerClassEntry,
936 LambdaTargetStub, MethodStub, ModuleExports, ModuleProvides, ModuleRequires, ModuleStub,
937 ReferenceKind, TypeParameterStub, TypeSignature,
938 };
939 use sqry_core::graph::unified::BidirectionalEdgeStore;
940 use sqry_core::graph::unified::storage::AuxiliaryIndices;
941 use sqry_core::graph::unified::storage::NodeArena;
942
943 fn make_interner() -> StringInterner {
948 StringInterner::new()
949 }
950
951 fn make_staging() -> StagingGraph {
952 StagingGraph::default()
953 }
954
955 fn make_provenance(jar: &str, direct: bool) -> ClasspathProvenance {
956 ClasspathProvenance {
957 jar_path: PathBuf::from(jar),
958 coordinates: Some(format!(
959 "group:artifact:{}",
960 if direct { "1.0" } else { "2.0" }
961 )),
962 is_direct: direct,
963 scopes: vec![crate::graph::provenance::ClasspathScope {
964 module_name: "test".to_owned(),
965 module_root: PathBuf::from("/repo/test"),
966 is_direct: direct,
967 }],
968 }
969 }
970
971 fn make_stub(fqn: &str) -> ClassStub {
972 ClassStub {
973 fqn: fqn.to_owned(),
974 name: fqn.rsplit('.').next().unwrap_or(fqn).to_owned(),
975 kind: ClassKind::Class,
976 access: AccessFlags::new(AccessFlags::ACC_PUBLIC),
977 superclass: Some("java.lang.Object".to_owned()),
978 interfaces: vec![],
979 methods: vec![],
980 fields: vec![],
981 annotations: vec![],
982 generic_signature: None,
983 inner_classes: vec![],
984 lambda_targets: vec![],
985 module: None,
986 record_components: vec![],
987 enum_constants: vec![],
988 source_file: None,
989 source_jar: None,
990 kotlin_metadata: None,
991 scala_signature: None,
992 }
993 }
994
995 fn make_method(name: &str) -> MethodStub {
996 MethodStub {
997 name: name.to_owned(),
998 access: AccessFlags::new(AccessFlags::ACC_PUBLIC),
999 descriptor: "()V".to_owned(),
1000 generic_signature: None,
1001 annotations: vec![],
1002 parameter_annotations: vec![],
1003 parameter_names: vec![],
1004 return_type: TypeSignature::Base(crate::stub::model::BaseType::Void),
1005 parameter_types: vec![],
1006 }
1007 }
1008
1009 fn make_field(name: &str) -> FieldStub {
1010 FieldStub {
1011 name: name.to_owned(),
1012 access: AccessFlags::new(AccessFlags::ACC_PRIVATE),
1013 descriptor: "I".to_owned(),
1014 generic_signature: None,
1015 annotations: vec![],
1016 constant_value: None,
1017 }
1018 }
1019
1020 fn run_emission(
1022 stubs: Vec<ClassStub>,
1023 provenance: &[ClasspathProvenance],
1024 ) -> (
1025 EmissionResult,
1026 StagingGraph,
1027 FileRegistry,
1028 StringInterner,
1029 NodeMetadataStore,
1030 ) {
1031 let default_jar = provenance
1032 .first()
1033 .map(|entry| entry.jar_path.display().to_string());
1034 let normalized_stubs = stubs
1035 .into_iter()
1036 .map(|mut stub| {
1037 if stub.source_jar.is_none() {
1038 stub.source_jar = default_jar.clone();
1039 }
1040 stub
1041 })
1042 .collect();
1043 let index = ClasspathIndex::build(normalized_stubs);
1044 let mut staging = make_staging();
1045 let mut file_registry = FileRegistry::new();
1046 let mut interner = make_interner();
1047 let mut metadata_store = NodeMetadataStore::new();
1048
1049 let result = emit_classpath_nodes(
1050 &index,
1051 &mut staging,
1052 &mut file_registry,
1053 &mut interner,
1054 &mut metadata_store,
1055 provenance,
1056 )
1057 .expect("emission should succeed");
1058
1059 (result, staging, file_registry, interner, metadata_store)
1060 }
1061
1062 #[test]
1067 fn test_simple_class_emits_nodes() {
1068 let mut stub = make_stub("com.example.Foo");
1069 stub.methods = vec![make_method("bar"), make_method("baz")];
1070 stub.fields = vec![make_field("count")];
1071
1072 let prov = vec![make_provenance("/jars/example.jar", true)];
1073 let (result, _staging, _registry, _interner, _meta) = run_emission(vec![stub], &prov);
1074
1075 assert!(
1077 result.fqn_to_node.contains_key("com.example.Foo"),
1078 "class node should be emitted"
1079 );
1080
1081 assert!(
1083 result.fqn_to_node.contains_key("com.example.Foo.bar()V"),
1084 "method 'bar' should be emitted"
1085 );
1086 assert!(
1087 result.fqn_to_node.contains_key("com.example.Foo.baz()V"),
1088 "method 'baz' should be emitted"
1089 );
1090
1091 assert!(
1093 result.fqn_to_node.contains_key("com.example.Foo.count"),
1094 "field 'count' should be emitted"
1095 );
1096
1097 assert_eq!(result.fqn_to_node.len(), 4);
1099 }
1100
1101 #[test]
1106 fn test_inheritance_edge_created() {
1107 let mut list = make_stub("java.util.AbstractList");
1108 list.superclass = Some("java.util.AbstractCollection".to_owned());
1109
1110 let collection = make_stub("java.util.AbstractCollection");
1111
1112 let prov = vec![make_provenance("/jars/rt.jar", true)];
1113 let stubs = vec![list, collection];
1114 let index = ClasspathIndex::build(stubs.clone());
1115 let (result, mut staging, _registry, _interner, _meta) = run_emission(stubs, &prov);
1116
1117 create_classpath_edges(&index, &result.fqn_to_nodes, &prov, &mut staging);
1118
1119 assert!(result.fqn_to_node.contains_key("java.util.AbstractList"));
1121 assert!(
1122 result
1123 .fqn_to_node
1124 .contains_key("java.util.AbstractCollection")
1125 );
1126
1127 let stats = staging.stats();
1129 assert!(
1131 stats.edges_staged > 0,
1132 "should have at least one edge staged"
1133 );
1134 }
1135
1136 #[test]
1141 fn test_implements_edge_created() {
1142 let mut array_list = make_stub("java.util.ArrayList");
1143 array_list.interfaces = vec![
1144 "java.util.List".to_owned(),
1145 "java.io.Serializable".to_owned(),
1146 ];
1147
1148 let list_iface = {
1149 let mut s = make_stub("java.util.List");
1150 s.kind = ClassKind::Interface;
1151 s
1152 };
1153
1154 let serializable = {
1155 let mut s = make_stub("java.io.Serializable");
1156 s.kind = ClassKind::Interface;
1157 s
1158 };
1159
1160 let prov = vec![make_provenance("/jars/rt.jar", true)];
1161 let stubs = vec![array_list, list_iface, serializable];
1162 let index = ClasspathIndex::build(stubs.clone());
1163 let (result, mut staging, _registry, _interner, _meta) = run_emission(stubs, &prov);
1164
1165 create_classpath_edges(&index, &result.fqn_to_nodes, &prov, &mut staging);
1166
1167 assert!(result.fqn_to_node.contains_key("java.util.ArrayList"));
1169 assert!(result.fqn_to_node.contains_key("java.util.List"));
1170 assert!(result.fqn_to_node.contains_key("java.io.Serializable"));
1171 }
1172
1173 #[test]
1174 fn test_emit_into_code_graph_commits_edges_and_metadata() {
1175 let mut child = make_stub("java.util.AbstractList");
1176 child.superclass = Some("java.util.AbstractCollection".to_owned());
1177 child.source_jar = Some("/jars/rt.jar".to_owned());
1178
1179 let mut parent = make_stub("java.util.AbstractCollection");
1180 parent.source_jar = Some("/jars/rt.jar".to_owned());
1181
1182 let index = ClasspathIndex::build(vec![child, parent]);
1183 let provenance = vec![make_provenance("/jars/rt.jar", true)];
1184 let mut graph = CodeGraph::new();
1185
1186 let result = emit_into_code_graph(&index, &mut graph, &provenance)
1187 .expect("in-place classpath emission should succeed");
1188
1189 let child_id = *result
1190 .fqn_to_node
1191 .get("java.util.AbstractList")
1192 .expect("child class node should be returned");
1193 let parent_id = *result
1194 .fqn_to_node
1195 .get("java.util.AbstractCollection")
1196 .expect("parent class node should be returned");
1197
1198 assert!(
1199 graph.nodes().get(child_id).is_some(),
1200 "child node should exist"
1201 );
1202 assert!(
1203 graph.edge_count() > 0,
1204 "structural classpath edges should be committed"
1205 );
1206 assert!(
1207 graph.edges().edges_from(child_id).iter().any(|edge| {
1208 matches!(edge.kind, EdgeKind::Inherits) && edge.target == parent_id
1209 }),
1210 "child class should have an inherits edge to its parent"
1211 );
1212 assert!(
1213 matches!(
1214 graph.macro_metadata().get_typed(child_id),
1215 Some(TypedMetadata::Classpath(_))
1216 ),
1217 "classpath metadata should remain attached after node-id remap"
1218 );
1219 }
1220
1221 #[test]
1222 fn test_emit_into_code_graph_preserves_graph_on_failure() {
1223 let mut strings = StringInterner::with_max_ids(2);
1224 let existing_name = strings.intern("existing").unwrap();
1225 let existing_qname = strings.intern("existing::node").unwrap();
1226
1227 let mut files = FileRegistry::new();
1228 let file_id = files.register(Path::new("/existing.rs")).unwrap();
1229
1230 let mut nodes = NodeArena::new();
1231 let existing_node = nodes
1232 .alloc(
1233 NodeEntry::new(NodeKind::Function, existing_name, file_id)
1234 .with_qualified_name(existing_qname),
1235 )
1236 .unwrap();
1237
1238 let mut graph = CodeGraph::from_components(
1239 nodes,
1240 BidirectionalEdgeStore::new(),
1241 strings,
1242 files,
1243 AuxiliaryIndices::new(),
1244 NodeMetadataStore::new(),
1245 );
1246 graph.macro_metadata_mut().insert_typed(
1247 existing_node,
1248 TypedMetadata::Classpath(ClasspathNodeMetadata {
1249 coordinates: Some("group:existing:1.0".to_owned()),
1250 jar_path: "/existing.jar".to_owned(),
1251 fqn: "existing::node".to_owned(),
1252 is_direct_dependency: true,
1253 }),
1254 );
1255
1256 let snapshot_before = graph.snapshot();
1257 let existing_path_before = snapshot_before
1258 .files()
1259 .resolve(file_id)
1260 .expect("existing file should resolve")
1261 .to_path_buf();
1262 let existing_meta_before = snapshot_before
1263 .macro_metadata()
1264 .get_typed(existing_node)
1265 .cloned();
1266
1267 let mut failing_stub = make_stub("com.example.WillFail");
1268 failing_stub.source_jar = Some("/jars/failing.jar".to_owned());
1269 let index = ClasspathIndex::build(vec![failing_stub]);
1270 let provenance = vec![make_provenance("/jars/failing.jar", true)];
1271
1272 let error = emit_into_code_graph(&index, &mut graph, &provenance)
1273 .expect_err("emission should fail when the cloned interner is full");
1274 assert!(
1275 error.to_string().contains("string intern failed"),
1276 "unexpected error: {error}"
1277 );
1278
1279 assert_eq!(graph.node_count(), snapshot_before.nodes().len());
1280 assert_eq!(graph.edge_count(), 0);
1281 assert_eq!(graph.strings().len(), snapshot_before.strings().len());
1282 assert_eq!(graph.files().len(), snapshot_before.files().len());
1283
1284 let surviving_node = graph
1285 .nodes()
1286 .get(existing_node)
1287 .expect("existing node must survive failed emission");
1288 assert_eq!(surviving_node.name, existing_name);
1289 assert_eq!(
1290 graph
1291 .files()
1292 .resolve(file_id)
1293 .map(|path| path.to_path_buf()),
1294 Some(existing_path_before)
1295 );
1296 assert_eq!(
1297 graph.macro_metadata().get_typed(existing_node),
1298 existing_meta_before.as_ref()
1299 );
1300 }
1301
1302 #[test]
1307 fn test_classpath_metadata_attached() {
1308 let stub = make_stub("com.google.common.collect.ImmutableList");
1309 let prov = vec![ClasspathProvenance {
1310 jar_path: PathBuf::from("/home/user/.m2/repository/guava-33.0.0.jar"),
1311 coordinates: Some("com.google.guava:guava:33.0.0".to_owned()),
1312 is_direct: true,
1313 scopes: vec![crate::graph::provenance::ClasspathScope {
1314 module_name: "test".to_owned(),
1315 module_root: PathBuf::from("/repo/test"),
1316 is_direct: true,
1317 }],
1318 }];
1319
1320 let (result, _staging, _registry, _interner, metadata_store) =
1321 run_emission(vec![stub], &prov);
1322
1323 let node_id = result
1324 .fqn_to_node
1325 .get("com.google.common.collect.ImmutableList")
1326 .expect("node should exist");
1327
1328 let metadata = metadata_store
1329 .get_typed(*node_id)
1330 .expect("metadata should be attached");
1331
1332 match metadata {
1333 TypedMetadata::Classpath(cp) => {
1334 assert_eq!(cp.fqn, "com.google.common.collect.ImmutableList");
1335 assert_eq!(
1336 cp.coordinates.as_deref(),
1337 Some("com.google.guava:guava:33.0.0")
1338 );
1339 assert!(cp.is_direct_dependency);
1340 assert!(cp.jar_path.contains("guava-33.0.0.jar"));
1341 }
1342 TypedMetadata::Macro(_) => panic!("expected Classpath metadata, got Macro"),
1343 }
1344 }
1345
1346 #[test]
1351 fn test_zero_spans_on_classpath_nodes() {
1352 let mut stub = make_stub("com.example.ZeroSpan");
1353 stub.methods = vec![make_method("doWork")];
1354
1355 let prov = vec![make_provenance("/jars/test.jar", true)];
1356 let (result, staging, _registry, _interner, _meta) = run_emission(vec![stub], &prov);
1357
1358 assert!(
1362 !result.fqn_to_node.is_empty(),
1363 "should have emitted at least one node"
1364 );
1365
1366 let stats = staging.stats();
1368 assert!(
1369 stats.nodes_staged >= 2,
1370 "should have at least 2 nodes (class + method)"
1371 );
1372 }
1373
1374 #[test]
1379 fn test_annotation_edges() {
1380 let mut stub = make_stub("com.example.MyService");
1381 stub.annotations = vec![AnnotationStub {
1382 type_fqn: "org.springframework.stereotype.Service".to_owned(),
1383 elements: vec![],
1384 is_runtime_visible: true,
1385 }];
1386
1387 let ann_type = {
1389 let mut s = make_stub("org.springframework.stereotype.Service");
1390 s.kind = ClassKind::Annotation;
1391 s
1392 };
1393
1394 let prov = vec![make_provenance("/jars/app.jar", true)];
1395 let stubs = vec![stub, ann_type];
1396 let index = ClasspathIndex::build(stubs.clone());
1397 let (result, mut staging, _registry, _interner, _meta) = run_emission(stubs, &prov);
1398
1399 create_classpath_edges(&index, &result.fqn_to_nodes, &prov, &mut staging);
1400
1401 assert!(result.fqn_to_node.contains_key("com.example.MyService"));
1403 assert!(
1404 result
1405 .fqn_to_node
1406 .contains_key("org.springframework.stereotype.Service")
1407 );
1408
1409 let stats = staging.stats();
1411 assert!(
1412 stats.edges_staged > 0,
1413 "should have annotation edges staged"
1414 );
1415 }
1416
1417 #[test]
1422 fn test_module_edges() {
1423 let provider_class = make_stub("com.example.spi.MyProvider");
1424 let exported_class = make_stub("com.example.api.MyApi");
1425
1426 let mut module_stub = make_stub("module-info");
1427 module_stub.kind = ClassKind::Module;
1428 module_stub.module = Some(ModuleStub {
1429 name: "com.example".to_owned(),
1430 access: AccessFlags::new(0),
1431 version: None,
1432 requires: vec![ModuleRequires {
1433 module_name: "java.base".to_owned(),
1434 access: AccessFlags::new(0),
1435 version: None,
1436 }],
1437 exports: vec![ModuleExports {
1438 package: "com.example.api".to_owned(),
1439 access: AccessFlags::new(0),
1440 to_modules: vec![],
1441 }],
1442 opens: vec![],
1443 provides: vec![ModuleProvides {
1444 service: "com.example.spi.SomeService".to_owned(),
1445 implementations: vec!["com.example.spi.MyProvider".to_owned()],
1446 }],
1447 uses: vec![],
1448 });
1449
1450 let prov = vec![make_provenance("/jars/example.jar", true)];
1451 let stubs = vec![module_stub, provider_class, exported_class];
1452 let index = ClasspathIndex::build(stubs.clone());
1453 let (result, mut staging, _registry, _interner, _meta) = run_emission(stubs, &prov);
1454
1455 create_classpath_edges(&index, &result.fqn_to_nodes, &prov, &mut staging);
1456
1457 assert!(
1459 result.fqn_to_node.contains_key("module:com.example"),
1460 "module node should be emitted"
1461 );
1462
1463 let stats = staging.stats();
1464 assert!(stats.edges_staged > 0, "should have module edges staged");
1465 }
1466
1467 #[test]
1472 fn test_enum_constants_emitted() {
1473 let mut stub = make_stub("java.time.DayOfWeek");
1474 stub.kind = ClassKind::Enum;
1475 stub.enum_constants = vec![
1476 "MONDAY".to_owned(),
1477 "TUESDAY".to_owned(),
1478 "WEDNESDAY".to_owned(),
1479 ];
1480
1481 let prov = vec![make_provenance("/jars/rt.jar", true)];
1482 let (result, _staging, _registry, _interner, _meta) = run_emission(vec![stub], &prov);
1483
1484 assert!(
1485 result
1486 .fqn_to_node
1487 .contains_key("java.time.DayOfWeek.MONDAY")
1488 );
1489 assert!(
1490 result
1491 .fqn_to_node
1492 .contains_key("java.time.DayOfWeek.TUESDAY")
1493 );
1494 assert!(
1495 result
1496 .fqn_to_node
1497 .contains_key("java.time.DayOfWeek.WEDNESDAY")
1498 );
1499 }
1500
1501 #[test]
1506 fn test_type_parameters_emitted() {
1507 let mut stub = make_stub("java.util.HashMap");
1508 stub.generic_signature = Some(GenericClassSignature {
1509 type_parameters: vec![
1510 TypeParameterStub {
1511 name: "K".to_owned(),
1512 class_bound: None,
1513 interface_bounds: vec![],
1514 },
1515 TypeParameterStub {
1516 name: "V".to_owned(),
1517 class_bound: None,
1518 interface_bounds: vec![],
1519 },
1520 ],
1521 superclass: TypeSignature::Class {
1522 fqn: "java.util.AbstractMap".to_owned(),
1523 type_arguments: vec![],
1524 },
1525 interfaces: vec![],
1526 });
1527
1528 let prov = vec![make_provenance("/jars/rt.jar", true)];
1529 let (result, _staging, _registry, _interner, _meta) = run_emission(vec![stub], &prov);
1530
1531 assert!(result.fqn_to_node.contains_key("java.util.HashMap.<K>"));
1532 assert!(result.fqn_to_node.contains_key("java.util.HashMap.<V>"));
1533 }
1534
1535 #[test]
1540 fn test_generic_bound_edges() {
1541 let comparable = make_stub("java.lang.Comparable");
1542
1543 let mut stub = make_stub("com.example.Sorted");
1544 stub.generic_signature = Some(GenericClassSignature {
1545 type_parameters: vec![TypeParameterStub {
1546 name: "T".to_owned(),
1547 class_bound: Some(TypeSignature::Class {
1548 fqn: "java.lang.Comparable".to_owned(),
1549 type_arguments: vec![],
1550 }),
1551 interface_bounds: vec![],
1552 }],
1553 superclass: TypeSignature::Class {
1554 fqn: "java.lang.Object".to_owned(),
1555 type_arguments: vec![],
1556 },
1557 interfaces: vec![],
1558 });
1559
1560 let prov = vec![make_provenance("/jars/rt.jar", true)];
1561 let stubs = vec![stub, comparable];
1562 let index = ClasspathIndex::build(stubs.clone());
1563 let (result, mut staging, _registry, _interner, _meta) = run_emission(stubs, &prov);
1564
1565 create_classpath_edges(&index, &result.fqn_to_nodes, &prov, &mut staging);
1566
1567 assert!(result.fqn_to_node.contains_key("com.example.Sorted.<T>"));
1569 assert!(result.fqn_to_node.contains_key("java.lang.Comparable"));
1570 }
1571
1572 #[test]
1577 fn test_lambda_targets_emitted() {
1578 let mut stub = make_stub("com.example.Processor");
1579 stub.lambda_targets = vec![LambdaTargetStub {
1580 owner_fqn: "java.lang.String".to_owned(),
1581 method_name: "toUpperCase".to_owned(),
1582 method_descriptor: "()Ljava/lang/String;".to_owned(),
1583 reference_kind: ReferenceKind::InvokeVirtual,
1584 }];
1585
1586 let prov = vec![make_provenance("/jars/app.jar", true)];
1587 let (result, _staging, _registry, _interner, _meta) = run_emission(vec![stub], &prov);
1588
1589 assert!(
1590 result
1591 .fqn_to_node
1592 .contains_key("com.example.Processor.lambda$toUpperCase")
1593 );
1594 }
1595
1596 #[test]
1601 fn test_inner_class_contains_edge() {
1602 let outer = {
1603 let mut s = make_stub("com.example.Outer");
1604 s.inner_classes = vec![InnerClassEntry {
1605 inner_fqn: "com.example.Outer.Inner".to_owned(),
1606 outer_fqn: Some("com.example.Outer".to_owned()),
1607 inner_name: Some("Inner".to_owned()),
1608 access: AccessFlags::new(AccessFlags::ACC_PUBLIC),
1609 }];
1610 s
1611 };
1612
1613 let inner = make_stub("com.example.Outer.Inner");
1614
1615 let prov = vec![make_provenance("/jars/app.jar", true)];
1616 let stubs = vec![outer, inner];
1617 let index = ClasspathIndex::build(stubs.clone());
1618 let (result, mut staging, _registry, _interner, _meta) = run_emission(stubs, &prov);
1619
1620 create_classpath_edges(&index, &result.fqn_to_nodes, &prov, &mut staging);
1621
1622 assert!(result.fqn_to_node.contains_key("com.example.Outer"));
1623 assert!(result.fqn_to_node.contains_key("com.example.Outer.Inner"));
1624 }
1625
1626 #[test]
1631 fn test_empty_index_no_nodes() {
1632 let prov: Vec<ClasspathProvenance> = vec![];
1633 let (result, staging, _registry, _interner, _meta) = run_emission(vec![], &prov);
1634
1635 assert!(result.fqn_to_node.is_empty());
1636 assert_eq!(staging.stats().nodes_staged, 0);
1637 }
1638
1639 #[test]
1644 fn test_synthetic_file_path_convention() {
1645 let stub = make_stub("com.example.Foo");
1646 let prov = vec![make_provenance("/jars/example.jar", true)];
1647 let (result, _staging, file_registry, _interner, _meta) = run_emission(vec![stub], &prov);
1648
1649 let file_id = result
1650 .file_id_map
1651 .get("com.example.Foo")
1652 .expect("file ID should exist");
1653
1654 let path = file_registry
1655 .resolve(*file_id)
1656 .expect("file should be resolvable");
1657
1658 let path_str = path.to_string_lossy();
1659 assert!(
1660 path_str.contains("!/com/example/Foo.class"),
1661 "synthetic path should follow JAR convention, got: {path_str}"
1662 );
1663 }
1664
1665 #[test]
1670 fn test_interface_kind_mapping() {
1671 let mut stub = make_stub("java.util.List");
1672 stub.kind = ClassKind::Interface;
1673
1674 let prov = vec![make_provenance("/jars/rt.jar", true)];
1675 let (result, _staging, _registry, _interner, _meta) = run_emission(vec![stub], &prov);
1676
1677 assert!(result.fqn_to_node.contains_key("java.util.List"));
1678 }
1679
1680 #[test]
1685 fn test_method_level_annotation_edge() {
1686 let override_ann = {
1687 let mut s = make_stub("java.lang.Override");
1688 s.kind = ClassKind::Annotation;
1689 s
1690 };
1691
1692 let mut stub = make_stub("com.example.Foo");
1693 stub.methods = vec![MethodStub {
1694 name: "toString".to_owned(),
1695 access: AccessFlags::new(AccessFlags::ACC_PUBLIC),
1696 descriptor: "()Ljava/lang/String;".to_owned(),
1697 generic_signature: None,
1698 annotations: vec![AnnotationStub {
1699 type_fqn: "java.lang.Override".to_owned(),
1700 elements: vec![],
1701 is_runtime_visible: true,
1702 }],
1703 parameter_annotations: vec![],
1704 parameter_names: vec![],
1705 return_type: TypeSignature::Base(crate::stub::model::BaseType::Void),
1706 parameter_types: vec![],
1707 }];
1708
1709 let prov = vec![make_provenance("/jars/rt.jar", true)];
1710 let stubs = vec![stub, override_ann];
1711 let index = ClasspathIndex::build(stubs.clone());
1712 let (result, mut staging, _registry, _interner, _meta) = run_emission(stubs, &prov);
1713
1714 create_classpath_edges(&index, &result.fqn_to_nodes, &prov, &mut staging);
1715
1716 assert!(
1717 result
1718 .fqn_to_node
1719 .contains_key("com.example.Foo.toString()Ljava/lang/String;")
1720 );
1721 assert!(result.fqn_to_node.contains_key("java.lang.Override"));
1722 }
1723
1724 #[test]
1729 fn test_no_provenance_still_emits() {
1730 let stub = make_stub("com.example.NoProv");
1731 let (result, _staging, _registry, _interner, _meta) = run_emission(vec![stub], &[]);
1732
1733 assert!(result.fqn_to_node.contains_key("com.example.NoProv"));
1734 }
1735
1736 #[test]
1741 fn test_visibility_mapping() {
1742 assert_eq!(
1743 access_to_visibility(&AccessFlags::new(AccessFlags::ACC_PUBLIC)),
1744 "public"
1745 );
1746 assert_eq!(
1747 access_to_visibility(&AccessFlags::new(AccessFlags::ACC_PROTECTED)),
1748 "protected"
1749 );
1750 assert_eq!(
1751 access_to_visibility(&AccessFlags::new(AccessFlags::ACC_PRIVATE)),
1752 "private"
1753 );
1754 assert_eq!(access_to_visibility(&AccessFlags::empty()), "package");
1755 }
1756}