1use std::collections::HashMap;
27use std::path::{Path, PathBuf};
28
29use sqry_core::graph::node::Language;
30use sqry_core::graph::unified::build::ExportMap;
31use sqry_core::graph::unified::build::StagingGraph;
32use sqry_core::graph::unified::edge::EdgeKind;
33use sqry_core::graph::unified::node::NodeKind;
34use sqry_core::graph::unified::storage::metadata::{
35 ClasspathNodeMetadata, NodeMetadata, NodeMetadataStore,
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
86fn access_to_visibility(access: &crate::stub::model::AccessFlags) -> &'static str {
92 if access.is_public() {
93 "public"
94 } else if access.is_protected() {
95 "protected"
96 } else if access.is_private() {
97 "private"
98 } else {
99 "package"
100 }
101}
102
103fn class_kind_to_node_kind(kind: ClassKind) -> NodeKind {
105 match kind {
106 ClassKind::Class | ClassKind::Record => NodeKind::Class,
107 ClassKind::Interface => NodeKind::Interface,
108 ClassKind::Enum => NodeKind::Enum,
109 ClassKind::Annotation => NodeKind::Annotation,
110 ClassKind::Module => NodeKind::JavaModule,
111 }
112}
113
114fn register_synthetic_file(
123 jar_path: &Path,
124 fqn: &str,
125 file_registry: &mut FileRegistry,
126) -> ClasspathResult<FileId> {
127 let class_path_str = fqn.replace('.', "/");
128 let synthetic_path = format!("{}!/{class_path_str}.class", jar_path.display());
129 let path = PathBuf::from(&synthetic_path);
130 file_registry
131 .register_external(&path, Some(Language::Java))
132 .map_err(|e| {
133 ClasspathError::EmissionError(format!(
134 "failed to register synthetic file for {fqn}: {e}"
135 ))
136 })
137}
138
139#[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 file_id_map: HashMap<String, FileId> = HashMap::new();
173
174 let prov_map: HashMap<&Path, &ClasspathProvenance> = provenance
176 .iter()
177 .map(|p| (p.jar_path.as_path(), p))
178 .collect();
179
180 for stub in &index.classes {
181 emit_class_stub(
182 stub,
183 staging,
184 file_registry,
185 &mut helper,
186 metadata_store,
187 &prov_map,
188 &mut fqn_to_node,
189 &mut file_id_map,
190 )?;
191 }
192
193 Ok(EmissionResult {
194 fqn_to_node,
195 file_id_map,
196 })
197}
198
199#[derive(Debug)]
201pub struct EmissionResult {
202 pub fqn_to_node: HashMap<String, NodeId>,
204 pub file_id_map: HashMap<String, FileId>,
206}
207
208#[allow(clippy::too_many_lines)]
210fn emit_class_stub(
211 stub: &ClassStub,
212 staging: &mut StagingGraph,
213 file_registry: &mut FileRegistry,
214 helper: &mut InternHelper<'_>,
215 metadata_store: &mut NodeMetadataStore,
216 prov_map: &HashMap<&Path, &ClasspathProvenance>,
217 fqn_to_node: &mut HashMap<String, NodeId>,
218 file_id_map: &mut HashMap<String, FileId>,
219) -> ClasspathResult<()> {
220 let jar_path = if let Some(ref src_jar) = stub.source_jar {
223 PathBuf::from(src_jar)
224 } else if let Some((&path, _)) = prov_map.iter().next() {
225 path.to_path_buf()
226 } else {
227 PathBuf::from(format!("<classpath>/{}.class", stub.fqn.replace('.', "/")))
228 };
229 let file_id = register_synthetic_file(&jar_path, &stub.fqn, file_registry)?;
230 file_id_map.insert(stub.fqn.clone(), file_id);
231
232 let node_kind = class_kind_to_node_kind(stub.kind);
234 let name_id = helper.intern(&stub.name)?;
235 let qname_id = helper.intern(&stub.fqn)?;
236 let vis_id = helper.intern(access_to_visibility(&stub.access))?;
237
238 let class_entry = NodeEntry::new(node_kind, name_id, file_id)
239 .with_qualified_name(qname_id)
240 .with_visibility(vis_id)
241 .with_static(stub.access.is_static())
242 .with_unsafe(false);
243
244 let class_node_id = staging.add_node(class_entry);
245 fqn_to_node.insert(stub.fqn.clone(), class_node_id);
246
247 let prov = find_provenance_for_jar(&jar_path, prov_map);
249 let cp_meta = ClasspathNodeMetadata {
250 coordinates: prov.and_then(|p| p.coordinates.clone()),
251 jar_path: jar_path.display().to_string(),
252 fqn: stub.fqn.clone(),
253 is_direct_dependency: prov.is_some_and(|p| p.is_direct),
254 };
255 metadata_store.insert_metadata(class_node_id, NodeMetadata::Classpath(cp_meta.clone()));
256
257 for method in &stub.methods {
259 let method_name_id = helper.intern(&method.name)?;
260 let method_fqn = format!("{}.{}{}", stub.fqn, method.name, method.descriptor);
263 let method_qname_id = helper.intern(&method_fqn)?;
264 let method_vis_id = helper.intern(access_to_visibility(&method.access))?;
265
266 let method_entry = NodeEntry::new(NodeKind::Method, method_name_id, file_id)
267 .with_qualified_name(method_qname_id)
268 .with_visibility(method_vis_id)
269 .with_static(method.access.is_static());
270
271 let method_node_id = staging.add_node(method_entry);
272 fqn_to_node.insert(method_fqn, method_node_id);
273 metadata_store.insert_metadata(method_node_id, NodeMetadata::Classpath(cp_meta.clone()));
274
275 staging.add_edge(class_node_id, method_node_id, EdgeKind::Defines, file_id);
277 }
278
279 for field in &stub.fields {
281 let field_name_id = helper.intern(&field.name)?;
282 let field_fqn = format!("{}.{}", stub.fqn, field.name);
283 let field_qname_id = helper.intern(&field_fqn)?;
284 let field_vis_id = helper.intern(access_to_visibility(&field.access))?;
285
286 let field_entry = NodeEntry::new(NodeKind::Property, field_name_id, file_id)
287 .with_qualified_name(field_qname_id)
288 .with_visibility(field_vis_id)
289 .with_static(field.access.is_static());
290
291 let field_node_id = staging.add_node(field_entry);
292 fqn_to_node.insert(field_fqn, field_node_id);
293 metadata_store.insert_metadata(field_node_id, NodeMetadata::Classpath(cp_meta.clone()));
294
295 staging.add_edge(class_node_id, field_node_id, EdgeKind::Defines, file_id);
297 }
298
299 for constant_name in &stub.enum_constants {
301 let const_name_id = helper.intern(constant_name)?;
302 let const_fqn = format!("{}.{constant_name}", stub.fqn);
303 let const_qname_id = helper.intern(&const_fqn)?;
304
305 let const_entry = NodeEntry::new(NodeKind::EnumConstant, const_name_id, file_id)
306 .with_qualified_name(const_qname_id)
307 .with_visibility(helper.intern("public")?);
308
309 let const_node_id = staging.add_node(const_entry);
310 fqn_to_node.insert(const_fqn, const_node_id);
311 metadata_store.insert_metadata(const_node_id, NodeMetadata::Classpath(cp_meta.clone()));
312
313 staging.add_edge(class_node_id, const_node_id, EdgeKind::Defines, file_id);
315 }
316
317 if let Some(ref gen_sig) = stub.generic_signature {
319 for tp in &gen_sig.type_parameters {
320 let tp_name_id = helper.intern(&tp.name)?;
321 let tp_fqn = format!("{}.<{}>", stub.fqn, tp.name);
322 let tp_qname_id = helper.intern(&tp_fqn)?;
323
324 let tp_entry = NodeEntry::new(NodeKind::TypeParameter, tp_name_id, file_id)
325 .with_qualified_name(tp_qname_id);
326
327 let tp_node_id = staging.add_node(tp_entry);
328 fqn_to_node.insert(tp_fqn, tp_node_id);
329 metadata_store.insert_metadata(tp_node_id, NodeMetadata::Classpath(cp_meta.clone()));
330
331 staging.add_edge(class_node_id, tp_node_id, EdgeKind::TypeArgument, file_id);
333 }
334 }
335
336 for lambda in &stub.lambda_targets {
338 let lambda_label = format!("{}::{}", lambda.owner_fqn, lambda.method_name);
339 let lambda_name_id = helper.intern(&lambda_label)?;
340 let lambda_fqn = format!("{}.lambda${}", stub.fqn, lambda.method_name);
341 let lambda_qname_id = helper.intern(&lambda_fqn)?;
342
343 let lambda_entry = NodeEntry::new(NodeKind::LambdaTarget, lambda_name_id, file_id)
344 .with_qualified_name(lambda_qname_id);
345
346 let lambda_node_id = staging.add_node(lambda_entry);
347 fqn_to_node.insert(lambda_fqn, lambda_node_id);
348 metadata_store.insert_metadata(lambda_node_id, NodeMetadata::Classpath(cp_meta.clone()));
349
350 staging.add_edge(class_node_id, lambda_node_id, EdgeKind::Contains, file_id);
352 }
353
354 for inner in &stub.inner_classes {
356 if inner.outer_fqn.as_deref() == Some(&stub.fqn) {
359 }
365 }
366
367 if let Some(ref module) = stub.module {
369 let mod_name_id = helper.intern(&module.name)?;
370 let mod_fqn = format!("module:{}", module.name);
371 let mod_qname_id = helper.intern(&mod_fqn)?;
372
373 let mod_entry = NodeEntry::new(NodeKind::JavaModule, mod_name_id, file_id)
374 .with_qualified_name(mod_qname_id);
375
376 let mod_node_id = staging.add_node(mod_entry);
377 fqn_to_node.insert(mod_fqn, mod_node_id);
378 metadata_store.insert_metadata(mod_node_id, NodeMetadata::Classpath(cp_meta.clone()));
379
380 staging.add_edge(class_node_id, mod_node_id, EdgeKind::Contains, file_id);
382 }
383
384 Ok(())
385}
386
387fn find_provenance_for_jar<'a>(
393 jar_path: &Path,
394 prov_map: &HashMap<&Path, &'a ClasspathProvenance>,
395) -> Option<&'a ClasspathProvenance> {
396 prov_map.get(jar_path).copied()
397}
398
399pub fn register_classpath_exports(
412 fqn_to_node: &HashMap<String, NodeId>,
413 export_map: &mut ExportMap,
414 provenance: &[ClasspathProvenance],
415 file_id_map: &HashMap<String, FileId>,
416 index: &ClasspathIndex,
417) {
418 let class_fqns: std::collections::HashSet<&str> =
420 index.classes.iter().map(|s| s.fqn.as_str()).collect();
421
422 let direct_jars: std::collections::HashSet<&Path> = provenance
424 .iter()
425 .filter(|p| p.is_direct)
426 .map(|p| p.jar_path.as_path())
427 .collect();
428
429 let transitive_jars: std::collections::HashSet<&Path> = provenance
430 .iter()
431 .filter(|p| !p.is_direct)
432 .map(|p| p.jar_path.as_path())
433 .collect();
434
435 register_exports_for_jars(
437 &class_fqns,
438 fqn_to_node,
439 export_map,
440 file_id_map,
441 &direct_jars,
442 provenance,
443 );
444
445 register_exports_for_jars(
447 &class_fqns,
448 fqn_to_node,
449 export_map,
450 file_id_map,
451 &transitive_jars,
452 provenance,
453 );
454}
455
456fn register_exports_for_jars(
458 class_fqns: &std::collections::HashSet<&str>,
459 fqn_to_node: &HashMap<String, NodeId>,
460 export_map: &mut ExportMap,
461 file_id_map: &HashMap<String, FileId>,
462 _jar_filter: &std::collections::HashSet<&Path>,
463 _provenance: &[ClasspathProvenance],
464) {
465 for fqn in class_fqns {
466 if let (Some(&node_id), Some(&file_id)) = (fqn_to_node.get(*fqn), file_id_map.get(*fqn)) {
467 export_map.register((*fqn).to_owned(), file_id, node_id);
468 }
469 }
470}
471
472#[allow(clippy::too_many_lines)]
483pub fn create_classpath_edges(
484 index: &ClasspathIndex,
485 fqn_to_node: &HashMap<String, NodeId>,
486 staging: &mut StagingGraph,
487 file_id_map: &HashMap<String, FileId>,
488) {
489 for stub in &index.classes {
490 let Some(&class_node_id) = fqn_to_node.get(&stub.fqn) else {
491 continue;
492 };
493 let Some(&file_id) = file_id_map.get(&stub.fqn) else {
494 continue;
495 };
496
497 if let Some(ref superclass_fqn) = stub.superclass
499 && superclass_fqn != "java.lang.Object"
500 {
501 if let Some(&super_node_id) = fqn_to_node.get(superclass_fqn) {
502 staging.add_edge(class_node_id, super_node_id, EdgeKind::Inherits, file_id);
503 } else {
504 log::debug!(
505 "classpath: skipping Inherits edge for {} → {} (target not in graph)",
506 stub.fqn,
507 superclass_fqn
508 );
509 }
510 }
511
512 for iface_fqn in &stub.interfaces {
514 if let Some(&iface_node_id) = fqn_to_node.get(iface_fqn) {
515 staging.add_edge(class_node_id, iface_node_id, EdgeKind::Implements, file_id);
516 } else {
517 log::debug!(
518 "classpath: skipping Implements edge for {} → {} (target not in graph)",
519 stub.fqn,
520 iface_fqn
521 );
522 }
523 }
524
525 if let Some(ref gen_sig) = stub.generic_signature {
527 for tp in &gen_sig.type_parameters {
528 let tp_fqn = format!("{}.<{}>", stub.fqn, tp.name);
529 let Some(&tp_node_id) = fqn_to_node.get(&tp_fqn) else {
530 continue;
531 };
532
533 if let Some(ref bound) = tp.class_bound
535 && let Some(bound_fqn) = extract_class_fqn_from_type_sig(bound)
536 && let Some(&bound_node_id) = fqn_to_node.get(bound_fqn)
537 {
538 staging.add_edge(tp_node_id, bound_node_id, EdgeKind::GenericBound, file_id);
539 }
540
541 for ibound in &tp.interface_bounds {
543 if let Some(bound_fqn) = extract_class_fqn_from_type_sig(ibound)
544 && let Some(&bound_node_id) = fqn_to_node.get(bound_fqn)
545 {
546 staging.add_edge(
547 tp_node_id,
548 bound_node_id,
549 EdgeKind::GenericBound,
550 file_id,
551 );
552 }
553 }
554 }
555 }
556
557 for ann in &stub.annotations {
559 if let Some(&ann_type_node_id) = fqn_to_node.get(&ann.type_fqn) {
560 staging.add_edge(
561 class_node_id,
562 ann_type_node_id,
563 EdgeKind::AnnotatedWith,
564 file_id,
565 );
566 }
567 }
568
569 for method in &stub.methods {
571 let method_fqn = format!("{}.{}{}", stub.fqn, method.name, method.descriptor);
572 if let Some(&method_node_id) = fqn_to_node.get(&method_fqn) {
573 for ann in &method.annotations {
574 if let Some(&ann_type_node_id) = fqn_to_node.get(&ann.type_fqn) {
575 staging.add_edge(
576 method_node_id,
577 ann_type_node_id,
578 EdgeKind::AnnotatedWith,
579 file_id,
580 );
581 }
582 }
583 }
584 }
585
586 for field in &stub.fields {
588 let field_fqn = format!("{}.{}", stub.fqn, field.name);
589 if let Some(&field_node_id) = fqn_to_node.get(&field_fqn) {
590 for ann in &field.annotations {
591 if let Some(&ann_type_node_id) = fqn_to_node.get(&ann.type_fqn) {
592 staging.add_edge(
593 field_node_id,
594 ann_type_node_id,
595 EdgeKind::AnnotatedWith,
596 file_id,
597 );
598 }
599 }
600 }
601 }
602
603 for inner in &stub.inner_classes {
605 if inner.outer_fqn.as_deref() == Some(&stub.fqn)
606 && let Some(&inner_node_id) = fqn_to_node.get(&inner.inner_fqn)
607 {
608 staging.add_edge(class_node_id, inner_node_id, EdgeKind::Contains, file_id);
609 }
610 }
611
612 if let Some(ref module) = stub.module {
614 let mod_fqn = format!("module:{}", module.name);
615 let Some(&mod_node_id) = fqn_to_node.get(&mod_fqn) else {
616 continue;
617 };
618
619 for req in &module.requires {
621 let req_mod_fqn = format!("module:{}", req.module_name);
622 if let Some(&req_node_id) = fqn_to_node.get(&req_mod_fqn) {
623 staging.add_edge(mod_node_id, req_node_id, EdgeKind::ModuleRequires, file_id);
624 }
625 }
626
627 for exp in &module.exports {
629 let pkg_classes = index.lookup_package(&exp.package);
631 for pkg_class in pkg_classes {
632 if let Some(&pkg_class_node_id) = fqn_to_node.get(&pkg_class.fqn) {
633 staging.add_edge(
634 mod_node_id,
635 pkg_class_node_id,
636 EdgeKind::ModuleExports,
637 file_id,
638 );
639 }
640 }
641 }
642
643 for opens in &module.opens {
645 let pkg_classes = index.lookup_package(&opens.package);
646 for pkg_class in pkg_classes {
647 if let Some(&pkg_class_node_id) = fqn_to_node.get(&pkg_class.fqn) {
648 staging.add_edge(
649 mod_node_id,
650 pkg_class_node_id,
651 EdgeKind::ModuleOpens,
652 file_id,
653 );
654 }
655 }
656 }
657
658 for provides in &module.provides {
660 for impl_fqn in &provides.implementations {
661 if let Some(&impl_node_id) = fqn_to_node.get(impl_fqn) {
662 staging.add_edge(
663 mod_node_id,
664 impl_node_id,
665 EdgeKind::ModuleProvides,
666 file_id,
667 );
668 }
669 }
670 }
671 }
672 }
673}
674
675fn extract_class_fqn_from_type_sig(sig: &crate::stub::model::TypeSignature) -> Option<&str> {
677 match sig {
678 crate::stub::model::TypeSignature::Class { fqn, .. } => Some(fqn.as_str()),
679 _ => None,
680 }
681}
682
683#[cfg(test)]
688mod tests {
689 use super::*;
690 use crate::stub::model::{
691 AccessFlags, AnnotationStub, ClassKind, FieldStub, GenericClassSignature, InnerClassEntry,
692 LambdaTargetStub, MethodStub, ModuleExports, ModuleProvides, ModuleRequires, ModuleStub,
693 ReferenceKind, TypeParameterStub, TypeSignature,
694 };
695
696 fn make_interner() -> StringInterner {
701 StringInterner::new()
702 }
703
704 fn make_staging() -> StagingGraph {
705 StagingGraph::default()
706 }
707
708 fn make_provenance(jar: &str, direct: bool) -> ClasspathProvenance {
709 ClasspathProvenance {
710 jar_path: PathBuf::from(jar),
711 coordinates: Some(format!(
712 "group:artifact:{}",
713 if direct { "1.0" } else { "2.0" }
714 )),
715 is_direct: direct,
716 }
717 }
718
719 fn make_stub(fqn: &str) -> ClassStub {
720 ClassStub {
721 fqn: fqn.to_owned(),
722 name: fqn.rsplit('.').next().unwrap_or(fqn).to_owned(),
723 kind: ClassKind::Class,
724 access: AccessFlags::new(AccessFlags::ACC_PUBLIC),
725 superclass: Some("java.lang.Object".to_owned()),
726 interfaces: vec![],
727 methods: vec![],
728 fields: vec![],
729 annotations: vec![],
730 generic_signature: None,
731 inner_classes: vec![],
732 lambda_targets: vec![],
733 module: None,
734 record_components: vec![],
735 enum_constants: vec![],
736 source_file: None,
737 source_jar: None,
738 kotlin_metadata: None,
739 scala_signature: None,
740 }
741 }
742
743 fn make_method(name: &str) -> MethodStub {
744 MethodStub {
745 name: name.to_owned(),
746 access: AccessFlags::new(AccessFlags::ACC_PUBLIC),
747 descriptor: "()V".to_owned(),
748 generic_signature: None,
749 annotations: vec![],
750 parameter_annotations: vec![],
751 parameter_names: vec![],
752 return_type: TypeSignature::Base(crate::stub::model::BaseType::Void),
753 parameter_types: vec![],
754 }
755 }
756
757 fn make_field(name: &str) -> FieldStub {
758 FieldStub {
759 name: name.to_owned(),
760 access: AccessFlags::new(AccessFlags::ACC_PRIVATE),
761 descriptor: "I".to_owned(),
762 generic_signature: None,
763 annotations: vec![],
764 constant_value: None,
765 }
766 }
767
768 fn run_emission(
770 stubs: Vec<ClassStub>,
771 provenance: &[ClasspathProvenance],
772 ) -> (
773 EmissionResult,
774 StagingGraph,
775 FileRegistry,
776 StringInterner,
777 NodeMetadataStore,
778 ) {
779 let index = ClasspathIndex::build(stubs);
780 let mut staging = make_staging();
781 let mut file_registry = FileRegistry::new();
782 let mut interner = make_interner();
783 let mut metadata_store = NodeMetadataStore::new();
784
785 let result = emit_classpath_nodes(
786 &index,
787 &mut staging,
788 &mut file_registry,
789 &mut interner,
790 &mut metadata_store,
791 provenance,
792 )
793 .expect("emission should succeed");
794
795 (result, staging, file_registry, interner, metadata_store)
796 }
797
798 #[test]
803 fn test_simple_class_emits_nodes() {
804 let mut stub = make_stub("com.example.Foo");
805 stub.methods = vec![make_method("bar"), make_method("baz")];
806 stub.fields = vec![make_field("count")];
807
808 let prov = vec![make_provenance("/jars/example.jar", true)];
809 let (result, _staging, _registry, _interner, _meta) = run_emission(vec![stub], &prov);
810
811 assert!(
813 result.fqn_to_node.contains_key("com.example.Foo"),
814 "class node should be emitted"
815 );
816
817 assert!(
819 result.fqn_to_node.contains_key("com.example.Foo.bar()V"),
820 "method 'bar' should be emitted"
821 );
822 assert!(
823 result.fqn_to_node.contains_key("com.example.Foo.baz()V"),
824 "method 'baz' should be emitted"
825 );
826
827 assert!(
829 result.fqn_to_node.contains_key("com.example.Foo.count"),
830 "field 'count' should be emitted"
831 );
832
833 assert_eq!(result.fqn_to_node.len(), 4);
835 }
836
837 #[test]
842 fn test_inheritance_edge_created() {
843 let mut list = make_stub("java.util.AbstractList");
844 list.superclass = Some("java.util.AbstractCollection".to_owned());
845
846 let collection = make_stub("java.util.AbstractCollection");
847
848 let prov = vec![make_provenance("/jars/rt.jar", true)];
849 let stubs = vec![list, collection];
850 let index = ClasspathIndex::build(stubs.clone());
851 let (result, mut staging, _registry, _interner, _meta) = run_emission(stubs, &prov);
852
853 create_classpath_edges(
854 &index,
855 &result.fqn_to_node,
856 &mut staging,
857 &result.file_id_map,
858 );
859
860 assert!(result.fqn_to_node.contains_key("java.util.AbstractList"));
862 assert!(
863 result
864 .fqn_to_node
865 .contains_key("java.util.AbstractCollection")
866 );
867
868 let stats = staging.stats();
870 assert!(
872 stats.edges_staged > 0,
873 "should have at least one edge staged"
874 );
875 }
876
877 #[test]
882 fn test_implements_edge_created() {
883 let mut array_list = make_stub("java.util.ArrayList");
884 array_list.interfaces = vec![
885 "java.util.List".to_owned(),
886 "java.io.Serializable".to_owned(),
887 ];
888
889 let list_iface = {
890 let mut s = make_stub("java.util.List");
891 s.kind = ClassKind::Interface;
892 s
893 };
894
895 let serializable = {
896 let mut s = make_stub("java.io.Serializable");
897 s.kind = ClassKind::Interface;
898 s
899 };
900
901 let prov = vec![make_provenance("/jars/rt.jar", true)];
902 let stubs = vec![array_list, list_iface, serializable];
903 let index = ClasspathIndex::build(stubs.clone());
904 let (result, mut staging, _registry, _interner, _meta) = run_emission(stubs, &prov);
905
906 create_classpath_edges(
907 &index,
908 &result.fqn_to_node,
909 &mut staging,
910 &result.file_id_map,
911 );
912
913 assert!(result.fqn_to_node.contains_key("java.util.ArrayList"));
915 assert!(result.fqn_to_node.contains_key("java.util.List"));
916 assert!(result.fqn_to_node.contains_key("java.io.Serializable"));
917 }
918
919 #[test]
924 fn test_export_map_registration_and_lookup() {
925 let stubs = vec![
926 make_stub("com.example.Alpha"),
927 make_stub("com.example.Beta"),
928 ];
929
930 let prov = vec![make_provenance("/jars/example.jar", true)];
931 let index = ClasspathIndex::build(stubs.clone());
932 let (result, _staging, _registry, _interner, _meta) = run_emission(stubs, &prov);
933
934 let mut export_map = ExportMap::new();
935 register_classpath_exports(
936 &result.fqn_to_node,
937 &mut export_map,
938 &prov,
939 &result.file_id_map,
940 &index,
941 );
942
943 let alpha = export_map.lookup("com.example.Alpha");
945 assert!(alpha.is_some(), "Alpha should be in ExportMap");
946
947 let beta = export_map.lookup("com.example.Beta");
948 assert!(beta.is_some(), "Beta should be in ExportMap");
949
950 let missing = export_map.lookup("com.example.DoesNotExist");
952 assert!(missing.is_none());
953 }
954
955 #[test]
960 fn test_fqn_precedence_direct_before_transitive() {
961 let stub = make_stub("com.example.Foo");
964 let prov_direct = make_provenance("/jars/direct.jar", true);
965 let prov_transitive = make_provenance("/jars/transitive.jar", false);
966
967 let index = ClasspathIndex::build(vec![stub]);
968 let (result, _staging, _registry, _interner, _meta) = run_emission(
969 index.classes.clone(),
970 &[prov_direct.clone(), prov_transitive],
971 );
972
973 let mut export_map = ExportMap::new();
974 register_classpath_exports(
975 &result.fqn_to_node,
976 &mut export_map,
977 &[prov_direct, make_provenance("/jars/transitive.jar", false)],
978 &result.file_id_map,
979 &index,
980 );
981
982 assert!(export_map.lookup("com.example.Foo").is_some());
984 }
985
986 #[test]
991 fn test_classpath_metadata_attached() {
992 let stub = make_stub("com.google.common.collect.ImmutableList");
993 let prov = vec![ClasspathProvenance {
994 jar_path: PathBuf::from("/home/user/.m2/repository/guava-33.0.0.jar"),
995 coordinates: Some("com.google.guava:guava:33.0.0".to_owned()),
996 is_direct: true,
997 }];
998
999 let (result, _staging, _registry, _interner, metadata_store) =
1000 run_emission(vec![stub], &prov);
1001
1002 let node_id = result
1003 .fqn_to_node
1004 .get("com.google.common.collect.ImmutableList")
1005 .expect("node should exist");
1006
1007 let metadata = metadata_store
1008 .get_metadata(*node_id)
1009 .expect("metadata should be attached");
1010
1011 match metadata {
1012 NodeMetadata::Classpath(cp) => {
1013 assert_eq!(cp.fqn, "com.google.common.collect.ImmutableList");
1014 assert_eq!(
1015 cp.coordinates.as_deref(),
1016 Some("com.google.guava:guava:33.0.0")
1017 );
1018 assert!(cp.is_direct_dependency);
1019 assert!(cp.jar_path.contains("guava-33.0.0.jar"));
1020 }
1021 NodeMetadata::Macro(_) => panic!("expected Classpath metadata, got Macro"),
1022 }
1023 }
1024
1025 #[test]
1030 fn test_zero_spans_on_classpath_nodes() {
1031 let mut stub = make_stub("com.example.ZeroSpan");
1032 stub.methods = vec![make_method("doWork")];
1033
1034 let prov = vec![make_provenance("/jars/test.jar", true)];
1035 let (result, staging, _registry, _interner, _meta) = run_emission(vec![stub], &prov);
1036
1037 assert!(
1041 !result.fqn_to_node.is_empty(),
1042 "should have emitted at least one node"
1043 );
1044
1045 let stats = staging.stats();
1047 assert!(
1048 stats.nodes_staged >= 2,
1049 "should have at least 2 nodes (class + method)"
1050 );
1051 }
1052
1053 #[test]
1058 fn test_annotation_edges() {
1059 let mut stub = make_stub("com.example.MyService");
1060 stub.annotations = vec![AnnotationStub {
1061 type_fqn: "org.springframework.stereotype.Service".to_owned(),
1062 elements: vec![],
1063 is_runtime_visible: true,
1064 }];
1065
1066 let ann_type = {
1068 let mut s = make_stub("org.springframework.stereotype.Service");
1069 s.kind = ClassKind::Annotation;
1070 s
1071 };
1072
1073 let prov = vec![make_provenance("/jars/app.jar", true)];
1074 let stubs = vec![stub, ann_type];
1075 let index = ClasspathIndex::build(stubs.clone());
1076 let (result, mut staging, _registry, _interner, _meta) = run_emission(stubs, &prov);
1077
1078 create_classpath_edges(
1079 &index,
1080 &result.fqn_to_node,
1081 &mut staging,
1082 &result.file_id_map,
1083 );
1084
1085 assert!(result.fqn_to_node.contains_key("com.example.MyService"));
1087 assert!(
1088 result
1089 .fqn_to_node
1090 .contains_key("org.springframework.stereotype.Service")
1091 );
1092
1093 let stats = staging.stats();
1095 assert!(
1096 stats.edges_staged > 0,
1097 "should have annotation edges staged"
1098 );
1099 }
1100
1101 #[test]
1106 fn test_module_edges() {
1107 let provider_class = make_stub("com.example.spi.MyProvider");
1108 let exported_class = make_stub("com.example.api.MyApi");
1109
1110 let mut module_stub = make_stub("module-info");
1111 module_stub.kind = ClassKind::Module;
1112 module_stub.module = Some(ModuleStub {
1113 name: "com.example".to_owned(),
1114 access: AccessFlags::new(0),
1115 version: None,
1116 requires: vec![ModuleRequires {
1117 module_name: "java.base".to_owned(),
1118 access: AccessFlags::new(0),
1119 version: None,
1120 }],
1121 exports: vec![ModuleExports {
1122 package: "com.example.api".to_owned(),
1123 access: AccessFlags::new(0),
1124 to_modules: vec![],
1125 }],
1126 opens: vec![],
1127 provides: vec![ModuleProvides {
1128 service: "com.example.spi.SomeService".to_owned(),
1129 implementations: vec!["com.example.spi.MyProvider".to_owned()],
1130 }],
1131 uses: vec![],
1132 });
1133
1134 let prov = vec![make_provenance("/jars/example.jar", true)];
1135 let stubs = vec![module_stub, provider_class, exported_class];
1136 let index = ClasspathIndex::build(stubs.clone());
1137 let (result, mut staging, _registry, _interner, _meta) = run_emission(stubs, &prov);
1138
1139 create_classpath_edges(
1140 &index,
1141 &result.fqn_to_node,
1142 &mut staging,
1143 &result.file_id_map,
1144 );
1145
1146 assert!(
1148 result.fqn_to_node.contains_key("module:com.example"),
1149 "module node should be emitted"
1150 );
1151
1152 let stats = staging.stats();
1153 assert!(stats.edges_staged > 0, "should have module edges staged");
1154 }
1155
1156 #[test]
1161 fn test_enum_constants_emitted() {
1162 let mut stub = make_stub("java.time.DayOfWeek");
1163 stub.kind = ClassKind::Enum;
1164 stub.enum_constants = vec![
1165 "MONDAY".to_owned(),
1166 "TUESDAY".to_owned(),
1167 "WEDNESDAY".to_owned(),
1168 ];
1169
1170 let prov = vec![make_provenance("/jars/rt.jar", true)];
1171 let (result, _staging, _registry, _interner, _meta) = run_emission(vec![stub], &prov);
1172
1173 assert!(
1174 result
1175 .fqn_to_node
1176 .contains_key("java.time.DayOfWeek.MONDAY")
1177 );
1178 assert!(
1179 result
1180 .fqn_to_node
1181 .contains_key("java.time.DayOfWeek.TUESDAY")
1182 );
1183 assert!(
1184 result
1185 .fqn_to_node
1186 .contains_key("java.time.DayOfWeek.WEDNESDAY")
1187 );
1188 }
1189
1190 #[test]
1195 fn test_type_parameters_emitted() {
1196 let mut stub = make_stub("java.util.HashMap");
1197 stub.generic_signature = Some(GenericClassSignature {
1198 type_parameters: vec![
1199 TypeParameterStub {
1200 name: "K".to_owned(),
1201 class_bound: None,
1202 interface_bounds: vec![],
1203 },
1204 TypeParameterStub {
1205 name: "V".to_owned(),
1206 class_bound: None,
1207 interface_bounds: vec![],
1208 },
1209 ],
1210 superclass: TypeSignature::Class {
1211 fqn: "java.util.AbstractMap".to_owned(),
1212 type_arguments: vec![],
1213 },
1214 interfaces: vec![],
1215 });
1216
1217 let prov = vec![make_provenance("/jars/rt.jar", true)];
1218 let (result, _staging, _registry, _interner, _meta) = run_emission(vec![stub], &prov);
1219
1220 assert!(result.fqn_to_node.contains_key("java.util.HashMap.<K>"));
1221 assert!(result.fqn_to_node.contains_key("java.util.HashMap.<V>"));
1222 }
1223
1224 #[test]
1229 fn test_generic_bound_edges() {
1230 let comparable = make_stub("java.lang.Comparable");
1231
1232 let mut stub = make_stub("com.example.Sorted");
1233 stub.generic_signature = Some(GenericClassSignature {
1234 type_parameters: vec![TypeParameterStub {
1235 name: "T".to_owned(),
1236 class_bound: Some(TypeSignature::Class {
1237 fqn: "java.lang.Comparable".to_owned(),
1238 type_arguments: vec![],
1239 }),
1240 interface_bounds: vec![],
1241 }],
1242 superclass: TypeSignature::Class {
1243 fqn: "java.lang.Object".to_owned(),
1244 type_arguments: vec![],
1245 },
1246 interfaces: vec![],
1247 });
1248
1249 let prov = vec![make_provenance("/jars/rt.jar", true)];
1250 let stubs = vec![stub, comparable];
1251 let index = ClasspathIndex::build(stubs.clone());
1252 let (result, mut staging, _registry, _interner, _meta) = run_emission(stubs, &prov);
1253
1254 create_classpath_edges(
1255 &index,
1256 &result.fqn_to_node,
1257 &mut staging,
1258 &result.file_id_map,
1259 );
1260
1261 assert!(result.fqn_to_node.contains_key("com.example.Sorted.<T>"));
1263 assert!(result.fqn_to_node.contains_key("java.lang.Comparable"));
1264 }
1265
1266 #[test]
1271 fn test_lambda_targets_emitted() {
1272 let mut stub = make_stub("com.example.Processor");
1273 stub.lambda_targets = vec![LambdaTargetStub {
1274 owner_fqn: "java.lang.String".to_owned(),
1275 method_name: "toUpperCase".to_owned(),
1276 method_descriptor: "()Ljava/lang/String;".to_owned(),
1277 reference_kind: ReferenceKind::InvokeVirtual,
1278 }];
1279
1280 let prov = vec![make_provenance("/jars/app.jar", true)];
1281 let (result, _staging, _registry, _interner, _meta) = run_emission(vec![stub], &prov);
1282
1283 assert!(
1284 result
1285 .fqn_to_node
1286 .contains_key("com.example.Processor.lambda$toUpperCase")
1287 );
1288 }
1289
1290 #[test]
1295 fn test_inner_class_contains_edge() {
1296 let outer = {
1297 let mut s = make_stub("com.example.Outer");
1298 s.inner_classes = vec![InnerClassEntry {
1299 inner_fqn: "com.example.Outer.Inner".to_owned(),
1300 outer_fqn: Some("com.example.Outer".to_owned()),
1301 inner_name: Some("Inner".to_owned()),
1302 access: AccessFlags::new(AccessFlags::ACC_PUBLIC),
1303 }];
1304 s
1305 };
1306
1307 let inner = make_stub("com.example.Outer.Inner");
1308
1309 let prov = vec![make_provenance("/jars/app.jar", true)];
1310 let stubs = vec![outer, inner];
1311 let index = ClasspathIndex::build(stubs.clone());
1312 let (result, mut staging, _registry, _interner, _meta) = run_emission(stubs, &prov);
1313
1314 create_classpath_edges(
1315 &index,
1316 &result.fqn_to_node,
1317 &mut staging,
1318 &result.file_id_map,
1319 );
1320
1321 assert!(result.fqn_to_node.contains_key("com.example.Outer"));
1322 assert!(result.fqn_to_node.contains_key("com.example.Outer.Inner"));
1323 }
1324
1325 #[test]
1330 fn test_empty_index_no_nodes() {
1331 let prov: Vec<ClasspathProvenance> = vec![];
1332 let (result, staging, _registry, _interner, _meta) = run_emission(vec![], &prov);
1333
1334 assert!(result.fqn_to_node.is_empty());
1335 assert_eq!(staging.stats().nodes_staged, 0);
1336 }
1337
1338 #[test]
1343 fn test_synthetic_file_path_convention() {
1344 let stub = make_stub("com.example.Foo");
1345 let prov = vec![make_provenance("/jars/example.jar", true)];
1346 let (result, _staging, file_registry, _interner, _meta) = run_emission(vec![stub], &prov);
1347
1348 let file_id = result
1349 .file_id_map
1350 .get("com.example.Foo")
1351 .expect("file ID should exist");
1352
1353 let path = file_registry
1354 .resolve(*file_id)
1355 .expect("file should be resolvable");
1356
1357 let path_str = path.to_string_lossy();
1358 assert!(
1359 path_str.contains("!/com/example/Foo.class"),
1360 "synthetic path should follow JAR convention, got: {path_str}"
1361 );
1362 }
1363
1364 #[test]
1369 fn test_interface_kind_mapping() {
1370 let mut stub = make_stub("java.util.List");
1371 stub.kind = ClassKind::Interface;
1372
1373 let prov = vec![make_provenance("/jars/rt.jar", true)];
1374 let (result, _staging, _registry, _interner, _meta) = run_emission(vec![stub], &prov);
1375
1376 assert!(result.fqn_to_node.contains_key("java.util.List"));
1377 }
1378
1379 #[test]
1384 fn test_method_level_annotation_edge() {
1385 let override_ann = {
1386 let mut s = make_stub("java.lang.Override");
1387 s.kind = ClassKind::Annotation;
1388 s
1389 };
1390
1391 let mut stub = make_stub("com.example.Foo");
1392 stub.methods = vec![MethodStub {
1393 name: "toString".to_owned(),
1394 access: AccessFlags::new(AccessFlags::ACC_PUBLIC),
1395 descriptor: "()Ljava/lang/String;".to_owned(),
1396 generic_signature: None,
1397 annotations: vec![AnnotationStub {
1398 type_fqn: "java.lang.Override".to_owned(),
1399 elements: vec![],
1400 is_runtime_visible: true,
1401 }],
1402 parameter_annotations: vec![],
1403 parameter_names: vec![],
1404 return_type: TypeSignature::Base(crate::stub::model::BaseType::Void),
1405 parameter_types: vec![],
1406 }];
1407
1408 let prov = vec![make_provenance("/jars/rt.jar", true)];
1409 let stubs = vec![stub, override_ann];
1410 let index = ClasspathIndex::build(stubs.clone());
1411 let (result, mut staging, _registry, _interner, _meta) = run_emission(stubs, &prov);
1412
1413 create_classpath_edges(
1414 &index,
1415 &result.fqn_to_node,
1416 &mut staging,
1417 &result.file_id_map,
1418 );
1419
1420 assert!(
1421 result
1422 .fqn_to_node
1423 .contains_key("com.example.Foo.toString()Ljava/lang/String;")
1424 );
1425 assert!(result.fqn_to_node.contains_key("java.lang.Override"));
1426 }
1427
1428 #[test]
1433 fn test_no_provenance_still_emits() {
1434 let stub = make_stub("com.example.NoProv");
1435 let (result, _staging, _registry, _interner, _meta) = run_emission(vec![stub], &[]);
1436
1437 assert!(result.fqn_to_node.contains_key("com.example.NoProv"));
1438 }
1439
1440 #[test]
1445 fn test_visibility_mapping() {
1446 assert_eq!(
1447 access_to_visibility(&AccessFlags::new(AccessFlags::ACC_PUBLIC)),
1448 "public"
1449 );
1450 assert_eq!(
1451 access_to_visibility(&AccessFlags::new(AccessFlags::ACC_PROTECTED)),
1452 "protected"
1453 );
1454 assert_eq!(
1455 access_to_visibility(&AccessFlags::new(AccessFlags::ACC_PRIVATE)),
1456 "private"
1457 );
1458 assert_eq!(access_to_visibility(&AccessFlags::empty()), "package");
1459 }
1460}