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