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