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
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)]
163pub fn emit_classpath_nodes(
164 index: &ClasspathIndex,
165 staging: &mut StagingGraph,
166 file_registry: &mut FileRegistry,
167 interner: &mut StringInterner,
168 metadata_store: &mut NodeMetadataStore,
169 provenance: &[ClasspathProvenance],
170) -> ClasspathResult<EmissionResult> {
171 let mut helper = InternHelper::new(interner);
172 let mut fqn_to_node: HashMap<String, NodeId> = HashMap::with_capacity(index.classes.len());
173 let mut file_id_map: HashMap<String, FileId> = HashMap::new();
174
175 let prov_map: HashMap<&Path, &ClasspathProvenance> = provenance
177 .iter()
178 .map(|p| (p.jar_path.as_path(), p))
179 .collect();
180
181 for stub in &index.classes {
182 emit_class_stub(
183 stub,
184 staging,
185 file_registry,
186 &mut helper,
187 metadata_store,
188 &prov_map,
189 &mut fqn_to_node,
190 &mut file_id_map,
191 )?;
192 }
193
194 Ok(EmissionResult {
195 fqn_to_node,
196 file_id_map,
197 })
198}
199
200#[derive(Debug)]
202pub struct EmissionResult {
203 pub fqn_to_node: HashMap<String, NodeId>,
205 pub file_id_map: HashMap<String, FileId>,
207}
208
209#[allow(clippy::too_many_lines)]
211#[allow(clippy::similar_names)] fn emit_class_stub(
213 stub: &ClassStub,
214 staging: &mut StagingGraph,
215 file_registry: &mut FileRegistry,
216 helper: &mut InternHelper<'_>,
217 metadata_store: &mut NodeMetadataStore,
218 prov_map: &HashMap<&Path, &ClasspathProvenance>,
219 fqn_to_node: &mut HashMap<String, NodeId>,
220 file_id_map: &mut HashMap<String, FileId>,
221) -> ClasspathResult<()> {
222 let jar_path = if let Some(ref src_jar) = stub.source_jar {
225 PathBuf::from(src_jar)
226 } else if let Some((&path, _)) = prov_map.iter().next() {
227 path.to_path_buf()
228 } else {
229 PathBuf::from(format!("<classpath>/{}.class", stub.fqn.replace('.', "/")))
230 };
231 let file_id = register_synthetic_file(&jar_path, &stub.fqn, file_registry)?;
232 file_id_map.insert(stub.fqn.clone(), file_id);
233
234 let node_kind = class_kind_to_node_kind(stub.kind);
236 let name_id = helper.intern(&stub.name)?;
237 let qname_id = helper.intern(&stub.fqn)?;
238 let vis_id = helper.intern(access_to_visibility(&stub.access))?;
239
240 let class_entry = NodeEntry::new(node_kind, name_id, file_id)
241 .with_qualified_name(qname_id)
242 .with_visibility(vis_id)
243 .with_static(stub.access.is_static())
244 .with_unsafe(false);
245
246 let class_node_id = staging.add_node(class_entry);
247 fqn_to_node.insert(stub.fqn.clone(), class_node_id);
248
249 let prov = find_provenance_for_jar(&jar_path, prov_map);
251 let cp_meta = ClasspathNodeMetadata {
252 coordinates: prov.and_then(|p| p.coordinates.clone()),
253 jar_path: jar_path.display().to_string(),
254 fqn: stub.fqn.clone(),
255 is_direct_dependency: prov.is_some_and(|p| p.is_direct),
256 };
257 metadata_store.insert_metadata(class_node_id, NodeMetadata::Classpath(cp_meta.clone()));
258
259 for method in &stub.methods {
261 let method_name_id = helper.intern(&method.name)?;
262 let method_fqn = format!("{}.{}{}", stub.fqn, method.name, method.descriptor);
265 #[allow(clippy::similar_names)] let method_qname_id = helper.intern(&method_fqn)?;
267 let method_vis_id = helper.intern(access_to_visibility(&method.access))?;
268
269 let method_entry = NodeEntry::new(NodeKind::Method, method_name_id, file_id)
270 .with_qualified_name(method_qname_id)
271 .with_visibility(method_vis_id)
272 .with_static(method.access.is_static());
273
274 let method_node_id = staging.add_node(method_entry);
275 fqn_to_node.insert(method_fqn, method_node_id);
276 metadata_store.insert_metadata(method_node_id, NodeMetadata::Classpath(cp_meta.clone()));
277
278 staging.add_edge(class_node_id, method_node_id, EdgeKind::Defines, file_id);
280 }
281
282 for field in &stub.fields {
284 let field_name_id = helper.intern(&field.name)?;
285 let field_fqn = format!("{}.{}", stub.fqn, field.name);
286 let field_qname_id = helper.intern(&field_fqn)?;
287 let field_vis_id = helper.intern(access_to_visibility(&field.access))?;
288
289 let field_entry = NodeEntry::new(NodeKind::Property, field_name_id, file_id)
290 .with_qualified_name(field_qname_id)
291 .with_visibility(field_vis_id)
292 .with_static(field.access.is_static());
293
294 let field_node_id = staging.add_node(field_entry);
295 fqn_to_node.insert(field_fqn, field_node_id);
296 metadata_store.insert_metadata(field_node_id, NodeMetadata::Classpath(cp_meta.clone()));
297
298 staging.add_edge(class_node_id, field_node_id, EdgeKind::Defines, file_id);
300 }
301
302 for constant_name in &stub.enum_constants {
304 let const_name_id = helper.intern(constant_name)?;
305 let const_fqn = format!("{}.{constant_name}", stub.fqn);
306 let const_qname_id = helper.intern(&const_fqn)?;
307
308 let const_entry = NodeEntry::new(NodeKind::EnumConstant, const_name_id, file_id)
309 .with_qualified_name(const_qname_id)
310 .with_visibility(helper.intern("public")?);
311
312 let const_node_id = staging.add_node(const_entry);
313 fqn_to_node.insert(const_fqn, const_node_id);
314 metadata_store.insert_metadata(const_node_id, NodeMetadata::Classpath(cp_meta.clone()));
315
316 staging.add_edge(class_node_id, const_node_id, EdgeKind::Defines, file_id);
318 }
319
320 if let Some(ref gen_sig) = stub.generic_signature {
322 for tp in &gen_sig.type_parameters {
323 let tp_name_id = helper.intern(&tp.name)?;
324 let tp_fqn = format!("{}.<{}>", stub.fqn, tp.name);
325 let tp_qname_id = helper.intern(&tp_fqn)?;
326
327 let tp_entry = NodeEntry::new(NodeKind::TypeParameter, tp_name_id, file_id)
328 .with_qualified_name(tp_qname_id);
329
330 let tp_node_id = staging.add_node(tp_entry);
331 fqn_to_node.insert(tp_fqn, tp_node_id);
332 metadata_store.insert_metadata(tp_node_id, NodeMetadata::Classpath(cp_meta.clone()));
333
334 staging.add_edge(class_node_id, tp_node_id, EdgeKind::TypeArgument, file_id);
336 }
337 }
338
339 for lambda in &stub.lambda_targets {
341 let lambda_label = format!("{}::{}", lambda.owner_fqn, lambda.method_name);
342 let lambda_name_id = helper.intern(&lambda_label)?;
343 let lambda_fqn = format!("{}.lambda${}", stub.fqn, lambda.method_name);
344 let lambda_qname_id = helper.intern(&lambda_fqn)?;
345
346 let lambda_entry = NodeEntry::new(NodeKind::LambdaTarget, lambda_name_id, file_id)
347 .with_qualified_name(lambda_qname_id);
348
349 let lambda_node_id = staging.add_node(lambda_entry);
350 fqn_to_node.insert(lambda_fqn, lambda_node_id);
351 metadata_store.insert_metadata(lambda_node_id, NodeMetadata::Classpath(cp_meta.clone()));
352
353 staging.add_edge(class_node_id, lambda_node_id, EdgeKind::Contains, file_id);
355 }
356
357 for inner in &stub.inner_classes {
359 if inner.outer_fqn.as_deref() == Some(&stub.fqn) {
362 }
368 }
369
370 if let Some(ref module) = stub.module {
372 let mod_name_id = helper.intern(&module.name)?;
373 let mod_fqn = format!("module:{}", module.name);
374 let mod_qname_id = helper.intern(&mod_fqn)?;
375
376 let mod_entry = NodeEntry::new(NodeKind::JavaModule, mod_name_id, file_id)
377 .with_qualified_name(mod_qname_id);
378
379 let mod_node_id = staging.add_node(mod_entry);
380 fqn_to_node.insert(mod_fqn, mod_node_id);
381 metadata_store.insert_metadata(mod_node_id, NodeMetadata::Classpath(cp_meta.clone()));
382
383 staging.add_edge(class_node_id, mod_node_id, EdgeKind::Contains, file_id);
385 }
386
387 Ok(())
388}
389
390fn find_provenance_for_jar<'a>(
396 jar_path: &Path,
397 prov_map: &HashMap<&Path, &'a ClasspathProvenance>,
398) -> Option<&'a ClasspathProvenance> {
399 prov_map.get(jar_path).copied()
400}
401
402#[allow(clippy::implicit_hasher)] pub fn register_classpath_exports(
416 fqn_to_node: &HashMap<String, NodeId>,
417 export_map: &mut ExportMap,
418 provenance: &[ClasspathProvenance],
419 file_id_map: &HashMap<String, FileId>,
420 index: &ClasspathIndex,
421) {
422 let class_fqns: std::collections::HashSet<&str> =
424 index.classes.iter().map(|s| s.fqn.as_str()).collect();
425
426 let direct_jars: std::collections::HashSet<&Path> = provenance
428 .iter()
429 .filter(|p| p.is_direct)
430 .map(|p| p.jar_path.as_path())
431 .collect();
432
433 let transitive_jars: std::collections::HashSet<&Path> = provenance
434 .iter()
435 .filter(|p| !p.is_direct)
436 .map(|p| p.jar_path.as_path())
437 .collect();
438
439 register_exports_for_jars(
441 &class_fqns,
442 fqn_to_node,
443 export_map,
444 file_id_map,
445 &direct_jars,
446 provenance,
447 );
448
449 register_exports_for_jars(
451 &class_fqns,
452 fqn_to_node,
453 export_map,
454 file_id_map,
455 &transitive_jars,
456 provenance,
457 );
458}
459
460fn register_exports_for_jars(
462 class_fqns: &std::collections::HashSet<&str>,
463 fqn_to_node: &HashMap<String, NodeId>,
464 export_map: &mut ExportMap,
465 file_id_map: &HashMap<String, FileId>,
466 _jar_filter: &std::collections::HashSet<&Path>,
467 _provenance: &[ClasspathProvenance],
468) {
469 for fqn in class_fqns {
470 if let (Some(&node_id), Some(&file_id)) = (fqn_to_node.get(*fqn), file_id_map.get(*fqn)) {
471 export_map.register((*fqn).to_owned(), file_id, node_id);
472 }
473 }
474}
475
476#[allow(clippy::too_many_lines)]
487#[allow(clippy::implicit_hasher)] pub fn create_classpath_edges(
489 #[allow(clippy::implicit_hasher)] index: &ClasspathIndex,
491 fqn_to_node: &HashMap<String, NodeId>,
492 staging: &mut StagingGraph,
493 file_id_map: &HashMap<String, FileId>,
494) {
495 for stub in &index.classes {
496 let Some(&class_node_id) = fqn_to_node.get(&stub.fqn) else {
497 continue;
498 };
499 let Some(&file_id) = file_id_map.get(&stub.fqn) else {
500 continue;
501 };
502
503 if let Some(ref superclass_fqn) = stub.superclass
505 && superclass_fqn != "java.lang.Object"
506 {
507 if let Some(&super_node_id) = fqn_to_node.get(superclass_fqn) {
508 staging.add_edge(class_node_id, super_node_id, EdgeKind::Inherits, file_id);
509 } else {
510 log::debug!(
511 "classpath: skipping Inherits edge for {} → {} (target not in graph)",
512 stub.fqn,
513 superclass_fqn
514 );
515 }
516 }
517
518 for iface_fqn in &stub.interfaces {
520 if let Some(&iface_node_id) = fqn_to_node.get(iface_fqn) {
521 staging.add_edge(class_node_id, iface_node_id, EdgeKind::Implements, file_id);
522 } else {
523 log::debug!(
524 "classpath: skipping Implements edge for {} → {} (target not in graph)",
525 stub.fqn,
526 iface_fqn
527 );
528 }
529 }
530
531 if let Some(ref gen_sig) = stub.generic_signature {
533 for tp in &gen_sig.type_parameters {
534 let tp_fqn = format!("{}.<{}>", stub.fqn, tp.name);
535 let Some(&tp_node_id) = fqn_to_node.get(&tp_fqn) else {
536 continue;
537 };
538
539 if let Some(ref bound) = tp.class_bound
541 && let Some(bound_fqn) = extract_class_fqn_from_type_sig(bound)
542 && let Some(&bound_node_id) = fqn_to_node.get(bound_fqn)
543 {
544 staging.add_edge(tp_node_id, bound_node_id, EdgeKind::GenericBound, file_id);
545 }
546
547 for ibound in &tp.interface_bounds {
549 if let Some(bound_fqn) = extract_class_fqn_from_type_sig(ibound)
550 && let Some(&bound_node_id) = fqn_to_node.get(bound_fqn)
551 {
552 staging.add_edge(
553 tp_node_id,
554 bound_node_id,
555 EdgeKind::GenericBound,
556 file_id,
557 );
558 }
559 }
560 }
561 }
562
563 for ann in &stub.annotations {
565 if let Some(&ann_type_node_id) = fqn_to_node.get(&ann.type_fqn) {
566 staging.add_edge(
567 class_node_id,
568 ann_type_node_id,
569 EdgeKind::AnnotatedWith,
570 file_id,
571 );
572 }
573 }
574
575 for method in &stub.methods {
577 let method_fqn = format!("{}.{}{}", stub.fqn, method.name, method.descriptor);
578 if let Some(&method_node_id) = fqn_to_node.get(&method_fqn) {
579 for ann in &method.annotations {
580 if let Some(&ann_type_node_id) = fqn_to_node.get(&ann.type_fqn) {
581 staging.add_edge(
582 method_node_id,
583 ann_type_node_id,
584 EdgeKind::AnnotatedWith,
585 file_id,
586 );
587 }
588 }
589 }
590 }
591
592 for field in &stub.fields {
594 let field_fqn = format!("{}.{}", stub.fqn, field.name);
595 if let Some(&field_node_id) = fqn_to_node.get(&field_fqn) {
596 for ann in &field.annotations {
597 if let Some(&ann_type_node_id) = fqn_to_node.get(&ann.type_fqn) {
598 staging.add_edge(
599 field_node_id,
600 ann_type_node_id,
601 EdgeKind::AnnotatedWith,
602 file_id,
603 );
604 }
605 }
606 }
607 }
608
609 for inner in &stub.inner_classes {
611 if inner.outer_fqn.as_deref() == Some(&stub.fqn)
612 && let Some(&inner_node_id) = fqn_to_node.get(&inner.inner_fqn)
613 {
614 staging.add_edge(class_node_id, inner_node_id, EdgeKind::Contains, file_id);
615 }
616 }
617
618 if let Some(ref module) = stub.module {
620 let mod_fqn = format!("module:{}", module.name);
621 let Some(&mod_node_id) = fqn_to_node.get(&mod_fqn) else {
622 continue;
623 };
624
625 for req in &module.requires {
627 let req_mod_fqn = format!("module:{}", req.module_name);
628 if let Some(&req_node_id) = fqn_to_node.get(&req_mod_fqn) {
629 staging.add_edge(mod_node_id, req_node_id, EdgeKind::ModuleRequires, file_id);
630 }
631 }
632
633 for exp in &module.exports {
635 let pkg_classes = index.lookup_package(&exp.package);
637 for pkg_class in pkg_classes {
638 if let Some(&pkg_class_node_id) = fqn_to_node.get(&pkg_class.fqn) {
639 staging.add_edge(
640 mod_node_id,
641 pkg_class_node_id,
642 EdgeKind::ModuleExports,
643 file_id,
644 );
645 }
646 }
647 }
648
649 for opens in &module.opens {
651 let pkg_classes = index.lookup_package(&opens.package);
652 for pkg_class in pkg_classes {
653 if let Some(&pkg_class_node_id) = fqn_to_node.get(&pkg_class.fqn) {
654 staging.add_edge(
655 mod_node_id,
656 pkg_class_node_id,
657 EdgeKind::ModuleOpens,
658 file_id,
659 );
660 }
661 }
662 }
663
664 for provides in &module.provides {
666 for impl_fqn in &provides.implementations {
667 if let Some(&impl_node_id) = fqn_to_node.get(impl_fqn) {
668 staging.add_edge(
669 mod_node_id,
670 impl_node_id,
671 EdgeKind::ModuleProvides,
672 file_id,
673 );
674 }
675 }
676 }
677 }
678 }
679}
680
681fn extract_class_fqn_from_type_sig(sig: &crate::stub::model::TypeSignature) -> Option<&str> {
683 match sig {
684 crate::stub::model::TypeSignature::Class { fqn, .. } => Some(fqn.as_str()),
685 _ => None,
686 }
687}
688
689#[cfg(test)]
694mod tests {
695 use super::*;
696 use crate::stub::model::{
697 AccessFlags, AnnotationStub, ClassKind, FieldStub, GenericClassSignature, InnerClassEntry,
698 LambdaTargetStub, MethodStub, ModuleExports, ModuleProvides, ModuleRequires, ModuleStub,
699 ReferenceKind, TypeParameterStub, TypeSignature,
700 };
701
702 fn make_interner() -> StringInterner {
707 StringInterner::new()
708 }
709
710 fn make_staging() -> StagingGraph {
711 StagingGraph::default()
712 }
713
714 fn make_provenance(jar: &str, direct: bool) -> ClasspathProvenance {
715 ClasspathProvenance {
716 jar_path: PathBuf::from(jar),
717 coordinates: Some(format!(
718 "group:artifact:{}",
719 if direct { "1.0" } else { "2.0" }
720 )),
721 is_direct: direct,
722 }
723 }
724
725 fn make_stub(fqn: &str) -> ClassStub {
726 ClassStub {
727 fqn: fqn.to_owned(),
728 name: fqn.rsplit('.').next().unwrap_or(fqn).to_owned(),
729 kind: ClassKind::Class,
730 access: AccessFlags::new(AccessFlags::ACC_PUBLIC),
731 superclass: Some("java.lang.Object".to_owned()),
732 interfaces: vec![],
733 methods: vec![],
734 fields: vec![],
735 annotations: vec![],
736 generic_signature: None,
737 inner_classes: vec![],
738 lambda_targets: vec![],
739 module: None,
740 record_components: vec![],
741 enum_constants: vec![],
742 source_file: None,
743 source_jar: None,
744 kotlin_metadata: None,
745 scala_signature: None,
746 }
747 }
748
749 fn make_method(name: &str) -> MethodStub {
750 MethodStub {
751 name: name.to_owned(),
752 access: AccessFlags::new(AccessFlags::ACC_PUBLIC),
753 descriptor: "()V".to_owned(),
754 generic_signature: None,
755 annotations: vec![],
756 parameter_annotations: vec![],
757 parameter_names: vec![],
758 return_type: TypeSignature::Base(crate::stub::model::BaseType::Void),
759 parameter_types: vec![],
760 }
761 }
762
763 fn make_field(name: &str) -> FieldStub {
764 FieldStub {
765 name: name.to_owned(),
766 access: AccessFlags::new(AccessFlags::ACC_PRIVATE),
767 descriptor: "I".to_owned(),
768 generic_signature: None,
769 annotations: vec![],
770 constant_value: None,
771 }
772 }
773
774 fn run_emission(
776 stubs: Vec<ClassStub>,
777 provenance: &[ClasspathProvenance],
778 ) -> (
779 EmissionResult,
780 StagingGraph,
781 FileRegistry,
782 StringInterner,
783 NodeMetadataStore,
784 ) {
785 let index = ClasspathIndex::build(stubs);
786 let mut staging = make_staging();
787 let mut file_registry = FileRegistry::new();
788 let mut interner = make_interner();
789 let mut metadata_store = NodeMetadataStore::new();
790
791 let result = emit_classpath_nodes(
792 &index,
793 &mut staging,
794 &mut file_registry,
795 &mut interner,
796 &mut metadata_store,
797 provenance,
798 )
799 .expect("emission should succeed");
800
801 (result, staging, file_registry, interner, metadata_store)
802 }
803
804 #[test]
809 fn test_simple_class_emits_nodes() {
810 let mut stub = make_stub("com.example.Foo");
811 stub.methods = vec![make_method("bar"), make_method("baz")];
812 stub.fields = vec![make_field("count")];
813
814 let prov = vec![make_provenance("/jars/example.jar", true)];
815 let (result, _staging, _registry, _interner, _meta) = run_emission(vec![stub], &prov);
816
817 assert!(
819 result.fqn_to_node.contains_key("com.example.Foo"),
820 "class node should be emitted"
821 );
822
823 assert!(
825 result.fqn_to_node.contains_key("com.example.Foo.bar()V"),
826 "method 'bar' should be emitted"
827 );
828 assert!(
829 result.fqn_to_node.contains_key("com.example.Foo.baz()V"),
830 "method 'baz' should be emitted"
831 );
832
833 assert!(
835 result.fqn_to_node.contains_key("com.example.Foo.count"),
836 "field 'count' should be emitted"
837 );
838
839 assert_eq!(result.fqn_to_node.len(), 4);
841 }
842
843 #[test]
848 fn test_inheritance_edge_created() {
849 let mut list = make_stub("java.util.AbstractList");
850 list.superclass = Some("java.util.AbstractCollection".to_owned());
851
852 let collection = make_stub("java.util.AbstractCollection");
853
854 let prov = vec![make_provenance("/jars/rt.jar", true)];
855 let stubs = vec![list, collection];
856 let index = ClasspathIndex::build(stubs.clone());
857 let (result, mut staging, _registry, _interner, _meta) = run_emission(stubs, &prov);
858
859 create_classpath_edges(
860 &index,
861 &result.fqn_to_node,
862 &mut staging,
863 &result.file_id_map,
864 );
865
866 assert!(result.fqn_to_node.contains_key("java.util.AbstractList"));
868 assert!(
869 result
870 .fqn_to_node
871 .contains_key("java.util.AbstractCollection")
872 );
873
874 let stats = staging.stats();
876 assert!(
878 stats.edges_staged > 0,
879 "should have at least one edge staged"
880 );
881 }
882
883 #[test]
888 fn test_implements_edge_created() {
889 let mut array_list = make_stub("java.util.ArrayList");
890 array_list.interfaces = vec![
891 "java.util.List".to_owned(),
892 "java.io.Serializable".to_owned(),
893 ];
894
895 let list_iface = {
896 let mut s = make_stub("java.util.List");
897 s.kind = ClassKind::Interface;
898 s
899 };
900
901 let serializable = {
902 let mut s = make_stub("java.io.Serializable");
903 s.kind = ClassKind::Interface;
904 s
905 };
906
907 let prov = vec![make_provenance("/jars/rt.jar", true)];
908 let stubs = vec![array_list, list_iface, serializable];
909 let index = ClasspathIndex::build(stubs.clone());
910 let (result, mut staging, _registry, _interner, _meta) = run_emission(stubs, &prov);
911
912 create_classpath_edges(
913 &index,
914 &result.fqn_to_node,
915 &mut staging,
916 &result.file_id_map,
917 );
918
919 assert!(result.fqn_to_node.contains_key("java.util.ArrayList"));
921 assert!(result.fqn_to_node.contains_key("java.util.List"));
922 assert!(result.fqn_to_node.contains_key("java.io.Serializable"));
923 }
924
925 #[test]
930 fn test_export_map_registration_and_lookup() {
931 let stubs = vec![
932 make_stub("com.example.Alpha"),
933 make_stub("com.example.Beta"),
934 ];
935
936 let prov = vec![make_provenance("/jars/example.jar", true)];
937 let index = ClasspathIndex::build(stubs.clone());
938 let (result, _staging, _registry, _interner, _meta) = run_emission(stubs, &prov);
939
940 let mut export_map = ExportMap::new();
941 register_classpath_exports(
942 &result.fqn_to_node,
943 &mut export_map,
944 &prov,
945 &result.file_id_map,
946 &index,
947 );
948
949 let alpha = export_map.lookup("com.example.Alpha");
951 assert!(alpha.is_some(), "Alpha should be in ExportMap");
952
953 let beta = export_map.lookup("com.example.Beta");
954 assert!(beta.is_some(), "Beta should be in ExportMap");
955
956 let missing = export_map.lookup("com.example.DoesNotExist");
958 assert!(missing.is_none());
959 }
960
961 #[test]
966 fn test_fqn_precedence_direct_before_transitive() {
967 let stub = make_stub("com.example.Foo");
970 let prov_direct = make_provenance("/jars/direct.jar", true);
971 let prov_transitive = make_provenance("/jars/transitive.jar", false);
972
973 let index = ClasspathIndex::build(vec![stub]);
974 let (result, _staging, _registry, _interner, _meta) = run_emission(
975 index.classes.clone(),
976 &[prov_direct.clone(), prov_transitive],
977 );
978
979 let mut export_map = ExportMap::new();
980 register_classpath_exports(
981 &result.fqn_to_node,
982 &mut export_map,
983 &[prov_direct, make_provenance("/jars/transitive.jar", false)],
984 &result.file_id_map,
985 &index,
986 );
987
988 assert!(export_map.lookup("com.example.Foo").is_some());
990 }
991
992 #[test]
997 fn test_classpath_metadata_attached() {
998 let stub = make_stub("com.google.common.collect.ImmutableList");
999 let prov = vec![ClasspathProvenance {
1000 jar_path: PathBuf::from("/home/user/.m2/repository/guava-33.0.0.jar"),
1001 coordinates: Some("com.google.guava:guava:33.0.0".to_owned()),
1002 is_direct: true,
1003 }];
1004
1005 let (result, _staging, _registry, _interner, metadata_store) =
1006 run_emission(vec![stub], &prov);
1007
1008 let node_id = result
1009 .fqn_to_node
1010 .get("com.google.common.collect.ImmutableList")
1011 .expect("node should exist");
1012
1013 let metadata = metadata_store
1014 .get_metadata(*node_id)
1015 .expect("metadata should be attached");
1016
1017 match metadata {
1018 NodeMetadata::Classpath(cp) => {
1019 assert_eq!(cp.fqn, "com.google.common.collect.ImmutableList");
1020 assert_eq!(
1021 cp.coordinates.as_deref(),
1022 Some("com.google.guava:guava:33.0.0")
1023 );
1024 assert!(cp.is_direct_dependency);
1025 assert!(cp.jar_path.contains("guava-33.0.0.jar"));
1026 }
1027 NodeMetadata::Macro(_) => panic!("expected Classpath metadata, got Macro"),
1028 }
1029 }
1030
1031 #[test]
1036 fn test_zero_spans_on_classpath_nodes() {
1037 let mut stub = make_stub("com.example.ZeroSpan");
1038 stub.methods = vec![make_method("doWork")];
1039
1040 let prov = vec![make_provenance("/jars/test.jar", true)];
1041 let (result, staging, _registry, _interner, _meta) = run_emission(vec![stub], &prov);
1042
1043 assert!(
1047 !result.fqn_to_node.is_empty(),
1048 "should have emitted at least one node"
1049 );
1050
1051 let stats = staging.stats();
1053 assert!(
1054 stats.nodes_staged >= 2,
1055 "should have at least 2 nodes (class + method)"
1056 );
1057 }
1058
1059 #[test]
1064 fn test_annotation_edges() {
1065 let mut stub = make_stub("com.example.MyService");
1066 stub.annotations = vec![AnnotationStub {
1067 type_fqn: "org.springframework.stereotype.Service".to_owned(),
1068 elements: vec![],
1069 is_runtime_visible: true,
1070 }];
1071
1072 let ann_type = {
1074 let mut s = make_stub("org.springframework.stereotype.Service");
1075 s.kind = ClassKind::Annotation;
1076 s
1077 };
1078
1079 let prov = vec![make_provenance("/jars/app.jar", true)];
1080 let stubs = vec![stub, ann_type];
1081 let index = ClasspathIndex::build(stubs.clone());
1082 let (result, mut staging, _registry, _interner, _meta) = run_emission(stubs, &prov);
1083
1084 create_classpath_edges(
1085 &index,
1086 &result.fqn_to_node,
1087 &mut staging,
1088 &result.file_id_map,
1089 );
1090
1091 assert!(result.fqn_to_node.contains_key("com.example.MyService"));
1093 assert!(
1094 result
1095 .fqn_to_node
1096 .contains_key("org.springframework.stereotype.Service")
1097 );
1098
1099 let stats = staging.stats();
1101 assert!(
1102 stats.edges_staged > 0,
1103 "should have annotation edges staged"
1104 );
1105 }
1106
1107 #[test]
1112 fn test_module_edges() {
1113 let provider_class = make_stub("com.example.spi.MyProvider");
1114 let exported_class = make_stub("com.example.api.MyApi");
1115
1116 let mut module_stub = make_stub("module-info");
1117 module_stub.kind = ClassKind::Module;
1118 module_stub.module = Some(ModuleStub {
1119 name: "com.example".to_owned(),
1120 access: AccessFlags::new(0),
1121 version: None,
1122 requires: vec![ModuleRequires {
1123 module_name: "java.base".to_owned(),
1124 access: AccessFlags::new(0),
1125 version: None,
1126 }],
1127 exports: vec![ModuleExports {
1128 package: "com.example.api".to_owned(),
1129 access: AccessFlags::new(0),
1130 to_modules: vec![],
1131 }],
1132 opens: vec![],
1133 provides: vec![ModuleProvides {
1134 service: "com.example.spi.SomeService".to_owned(),
1135 implementations: vec!["com.example.spi.MyProvider".to_owned()],
1136 }],
1137 uses: vec![],
1138 });
1139
1140 let prov = vec![make_provenance("/jars/example.jar", true)];
1141 let stubs = vec![module_stub, provider_class, exported_class];
1142 let index = ClasspathIndex::build(stubs.clone());
1143 let (result, mut staging, _registry, _interner, _meta) = run_emission(stubs, &prov);
1144
1145 create_classpath_edges(
1146 &index,
1147 &result.fqn_to_node,
1148 &mut staging,
1149 &result.file_id_map,
1150 );
1151
1152 assert!(
1154 result.fqn_to_node.contains_key("module:com.example"),
1155 "module node should be emitted"
1156 );
1157
1158 let stats = staging.stats();
1159 assert!(stats.edges_staged > 0, "should have module edges staged");
1160 }
1161
1162 #[test]
1167 fn test_enum_constants_emitted() {
1168 let mut stub = make_stub("java.time.DayOfWeek");
1169 stub.kind = ClassKind::Enum;
1170 stub.enum_constants = vec![
1171 "MONDAY".to_owned(),
1172 "TUESDAY".to_owned(),
1173 "WEDNESDAY".to_owned(),
1174 ];
1175
1176 let prov = vec![make_provenance("/jars/rt.jar", true)];
1177 let (result, _staging, _registry, _interner, _meta) = run_emission(vec![stub], &prov);
1178
1179 assert!(
1180 result
1181 .fqn_to_node
1182 .contains_key("java.time.DayOfWeek.MONDAY")
1183 );
1184 assert!(
1185 result
1186 .fqn_to_node
1187 .contains_key("java.time.DayOfWeek.TUESDAY")
1188 );
1189 assert!(
1190 result
1191 .fqn_to_node
1192 .contains_key("java.time.DayOfWeek.WEDNESDAY")
1193 );
1194 }
1195
1196 #[test]
1201 fn test_type_parameters_emitted() {
1202 let mut stub = make_stub("java.util.HashMap");
1203 stub.generic_signature = Some(GenericClassSignature {
1204 type_parameters: vec![
1205 TypeParameterStub {
1206 name: "K".to_owned(),
1207 class_bound: None,
1208 interface_bounds: vec![],
1209 },
1210 TypeParameterStub {
1211 name: "V".to_owned(),
1212 class_bound: None,
1213 interface_bounds: vec![],
1214 },
1215 ],
1216 superclass: TypeSignature::Class {
1217 fqn: "java.util.AbstractMap".to_owned(),
1218 type_arguments: vec![],
1219 },
1220 interfaces: vec![],
1221 });
1222
1223 let prov = vec![make_provenance("/jars/rt.jar", true)];
1224 let (result, _staging, _registry, _interner, _meta) = run_emission(vec![stub], &prov);
1225
1226 assert!(result.fqn_to_node.contains_key("java.util.HashMap.<K>"));
1227 assert!(result.fqn_to_node.contains_key("java.util.HashMap.<V>"));
1228 }
1229
1230 #[test]
1235 fn test_generic_bound_edges() {
1236 let comparable = make_stub("java.lang.Comparable");
1237
1238 let mut stub = make_stub("com.example.Sorted");
1239 stub.generic_signature = Some(GenericClassSignature {
1240 type_parameters: vec![TypeParameterStub {
1241 name: "T".to_owned(),
1242 class_bound: Some(TypeSignature::Class {
1243 fqn: "java.lang.Comparable".to_owned(),
1244 type_arguments: vec![],
1245 }),
1246 interface_bounds: vec![],
1247 }],
1248 superclass: TypeSignature::Class {
1249 fqn: "java.lang.Object".to_owned(),
1250 type_arguments: vec![],
1251 },
1252 interfaces: vec![],
1253 });
1254
1255 let prov = vec![make_provenance("/jars/rt.jar", true)];
1256 let stubs = vec![stub, comparable];
1257 let index = ClasspathIndex::build(stubs.clone());
1258 let (result, mut staging, _registry, _interner, _meta) = run_emission(stubs, &prov);
1259
1260 create_classpath_edges(
1261 &index,
1262 &result.fqn_to_node,
1263 &mut staging,
1264 &result.file_id_map,
1265 );
1266
1267 assert!(result.fqn_to_node.contains_key("com.example.Sorted.<T>"));
1269 assert!(result.fqn_to_node.contains_key("java.lang.Comparable"));
1270 }
1271
1272 #[test]
1277 fn test_lambda_targets_emitted() {
1278 let mut stub = make_stub("com.example.Processor");
1279 stub.lambda_targets = vec![LambdaTargetStub {
1280 owner_fqn: "java.lang.String".to_owned(),
1281 method_name: "toUpperCase".to_owned(),
1282 method_descriptor: "()Ljava/lang/String;".to_owned(),
1283 reference_kind: ReferenceKind::InvokeVirtual,
1284 }];
1285
1286 let prov = vec![make_provenance("/jars/app.jar", true)];
1287 let (result, _staging, _registry, _interner, _meta) = run_emission(vec![stub], &prov);
1288
1289 assert!(
1290 result
1291 .fqn_to_node
1292 .contains_key("com.example.Processor.lambda$toUpperCase")
1293 );
1294 }
1295
1296 #[test]
1301 fn test_inner_class_contains_edge() {
1302 let outer = {
1303 let mut s = make_stub("com.example.Outer");
1304 s.inner_classes = vec![InnerClassEntry {
1305 inner_fqn: "com.example.Outer.Inner".to_owned(),
1306 outer_fqn: Some("com.example.Outer".to_owned()),
1307 inner_name: Some("Inner".to_owned()),
1308 access: AccessFlags::new(AccessFlags::ACC_PUBLIC),
1309 }];
1310 s
1311 };
1312
1313 let inner = make_stub("com.example.Outer.Inner");
1314
1315 let prov = vec![make_provenance("/jars/app.jar", true)];
1316 let stubs = vec![outer, inner];
1317 let index = ClasspathIndex::build(stubs.clone());
1318 let (result, mut staging, _registry, _interner, _meta) = run_emission(stubs, &prov);
1319
1320 create_classpath_edges(
1321 &index,
1322 &result.fqn_to_node,
1323 &mut staging,
1324 &result.file_id_map,
1325 );
1326
1327 assert!(result.fqn_to_node.contains_key("com.example.Outer"));
1328 assert!(result.fqn_to_node.contains_key("com.example.Outer.Inner"));
1329 }
1330
1331 #[test]
1336 fn test_empty_index_no_nodes() {
1337 let prov: Vec<ClasspathProvenance> = vec![];
1338 let (result, staging, _registry, _interner, _meta) = run_emission(vec![], &prov);
1339
1340 assert!(result.fqn_to_node.is_empty());
1341 assert_eq!(staging.stats().nodes_staged, 0);
1342 }
1343
1344 #[test]
1349 fn test_synthetic_file_path_convention() {
1350 let stub = make_stub("com.example.Foo");
1351 let prov = vec![make_provenance("/jars/example.jar", true)];
1352 let (result, _staging, file_registry, _interner, _meta) = run_emission(vec![stub], &prov);
1353
1354 let file_id = result
1355 .file_id_map
1356 .get("com.example.Foo")
1357 .expect("file ID should exist");
1358
1359 let path = file_registry
1360 .resolve(*file_id)
1361 .expect("file should be resolvable");
1362
1363 let path_str = path.to_string_lossy();
1364 assert!(
1365 path_str.contains("!/com/example/Foo.class"),
1366 "synthetic path should follow JAR convention, got: {path_str}"
1367 );
1368 }
1369
1370 #[test]
1375 fn test_interface_kind_mapping() {
1376 let mut stub = make_stub("java.util.List");
1377 stub.kind = ClassKind::Interface;
1378
1379 let prov = vec![make_provenance("/jars/rt.jar", true)];
1380 let (result, _staging, _registry, _interner, _meta) = run_emission(vec![stub], &prov);
1381
1382 assert!(result.fqn_to_node.contains_key("java.util.List"));
1383 }
1384
1385 #[test]
1390 fn test_method_level_annotation_edge() {
1391 let override_ann = {
1392 let mut s = make_stub("java.lang.Override");
1393 s.kind = ClassKind::Annotation;
1394 s
1395 };
1396
1397 let mut stub = make_stub("com.example.Foo");
1398 stub.methods = vec![MethodStub {
1399 name: "toString".to_owned(),
1400 access: AccessFlags::new(AccessFlags::ACC_PUBLIC),
1401 descriptor: "()Ljava/lang/String;".to_owned(),
1402 generic_signature: None,
1403 annotations: vec![AnnotationStub {
1404 type_fqn: "java.lang.Override".to_owned(),
1405 elements: vec![],
1406 is_runtime_visible: true,
1407 }],
1408 parameter_annotations: vec![],
1409 parameter_names: vec![],
1410 return_type: TypeSignature::Base(crate::stub::model::BaseType::Void),
1411 parameter_types: vec![],
1412 }];
1413
1414 let prov = vec![make_provenance("/jars/rt.jar", true)];
1415 let stubs = vec![stub, override_ann];
1416 let index = ClasspathIndex::build(stubs.clone());
1417 let (result, mut staging, _registry, _interner, _meta) = run_emission(stubs, &prov);
1418
1419 create_classpath_edges(
1420 &index,
1421 &result.fqn_to_node,
1422 &mut staging,
1423 &result.file_id_map,
1424 );
1425
1426 assert!(
1427 result
1428 .fqn_to_node
1429 .contains_key("com.example.Foo.toString()Ljava/lang/String;")
1430 );
1431 assert!(result.fqn_to_node.contains_key("java.lang.Override"));
1432 }
1433
1434 #[test]
1439 fn test_no_provenance_still_emits() {
1440 let stub = make_stub("com.example.NoProv");
1441 let (result, _staging, _registry, _interner, _meta) = run_emission(vec![stub], &[]);
1442
1443 assert!(result.fqn_to_node.contains_key("com.example.NoProv"));
1444 }
1445
1446 #[test]
1451 fn test_visibility_mapping() {
1452 assert_eq!(
1453 access_to_visibility(&AccessFlags::new(AccessFlags::ACC_PUBLIC)),
1454 "public"
1455 );
1456 assert_eq!(
1457 access_to_visibility(&AccessFlags::new(AccessFlags::ACC_PROTECTED)),
1458 "protected"
1459 );
1460 assert_eq!(
1461 access_to_visibility(&AccessFlags::new(AccessFlags::ACC_PRIVATE)),
1462 "private"
1463 );
1464 assert_eq!(access_to_visibility(&AccessFlags::empty()), "package");
1465 }
1466}