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 .with_definition(true);
382
383 let class_node_id = staging.add_node(class_entry);
384 record_node_ref(
385 &stub.fqn,
386 class_node_id,
387 &jar_path,
388 file_id,
389 fqn_to_node,
390 fqn_to_nodes,
391 );
392
393 let prov = find_provenance_for_jar(&jar_path, prov_map);
395 let cp_meta = ClasspathNodeMetadata {
396 coordinates: prov.and_then(|p| p.coordinates.clone()),
397 jar_path: jar_path.display().to_string(),
398 fqn: stub.fqn.clone(),
399 is_direct_dependency: prov.is_some_and(ClasspathProvenance::has_direct_scope),
400 };
401 metadata_store.insert_typed(class_node_id, TypedMetadata::Classpath(cp_meta.clone()));
402
403 for method in &stub.methods {
405 let method_name_id = helper.intern(&method.name)?;
406 let method_fqn = format!("{}.{}{}", stub.fqn, method.name, method.descriptor);
409 #[allow(clippy::similar_names)] let method_qname_id = helper.intern(&method_fqn)?;
411 let method_vis_id = helper.intern(access_to_visibility(&method.access))?;
412
413 let method_entry = NodeEntry::new(NodeKind::Method, method_name_id, file_id)
414 .with_qualified_name(method_qname_id)
415 .with_visibility(method_vis_id)
416 .with_static(method.access.is_static())
417 .with_definition(true);
418
419 let method_node_id = staging.add_node(method_entry);
420 record_node_ref(
421 &method_fqn,
422 method_node_id,
423 &jar_path,
424 file_id,
425 fqn_to_node,
426 fqn_to_nodes,
427 );
428 metadata_store.insert_typed(method_node_id, TypedMetadata::Classpath(cp_meta.clone()));
429
430 staging.add_edge(class_node_id, method_node_id, EdgeKind::Defines, file_id);
432 }
433
434 for field in &stub.fields {
436 let field_name_id = helper.intern(&field.name)?;
437 let field_fqn = format!("{}.{}", stub.fqn, field.name);
438 let field_qname_id = helper.intern(&field_fqn)?;
439 let field_vis_id = helper.intern(access_to_visibility(&field.access))?;
440
441 let field_entry = NodeEntry::new(NodeKind::Property, field_name_id, file_id)
442 .with_qualified_name(field_qname_id)
443 .with_visibility(field_vis_id)
444 .with_static(field.access.is_static())
445 .with_definition(true);
446
447 let field_node_id = staging.add_node(field_entry);
448 record_node_ref(
449 &field_fqn,
450 field_node_id,
451 &jar_path,
452 file_id,
453 fqn_to_node,
454 fqn_to_nodes,
455 );
456 metadata_store.insert_typed(field_node_id, TypedMetadata::Classpath(cp_meta.clone()));
457
458 staging.add_edge(class_node_id, field_node_id, EdgeKind::Defines, file_id);
460 }
461
462 for constant_name in &stub.enum_constants {
464 let const_name_id = helper.intern(constant_name)?;
465 let const_fqn = format!("{}.{constant_name}", stub.fqn);
466 let const_qname_id = helper.intern(&const_fqn)?;
467
468 let const_entry = NodeEntry::new(NodeKind::EnumConstant, const_name_id, file_id)
469 .with_qualified_name(const_qname_id)
470 .with_visibility(helper.intern("public")?)
471 .with_definition(true);
472
473 let const_node_id = staging.add_node(const_entry);
474 record_node_ref(
475 &const_fqn,
476 const_node_id,
477 &jar_path,
478 file_id,
479 fqn_to_node,
480 fqn_to_nodes,
481 );
482 metadata_store.insert_typed(const_node_id, TypedMetadata::Classpath(cp_meta.clone()));
483
484 staging.add_edge(class_node_id, const_node_id, EdgeKind::Defines, file_id);
486 }
487
488 if let Some(ref gen_sig) = stub.generic_signature {
490 for tp in &gen_sig.type_parameters {
491 let tp_name_id = helper.intern(&tp.name)?;
492 let tp_fqn = format!("{}.<{}>", stub.fqn, tp.name);
493 let tp_qname_id = helper.intern(&tp_fqn)?;
494
495 let tp_entry = NodeEntry::new(NodeKind::TypeParameter, tp_name_id, file_id)
496 .with_qualified_name(tp_qname_id)
497 .with_definition(true);
498
499 let tp_node_id = staging.add_node(tp_entry);
500 record_node_ref(
501 &tp_fqn,
502 tp_node_id,
503 &jar_path,
504 file_id,
505 fqn_to_node,
506 fqn_to_nodes,
507 );
508 metadata_store.insert_typed(tp_node_id, TypedMetadata::Classpath(cp_meta.clone()));
509
510 staging.add_edge(class_node_id, tp_node_id, EdgeKind::TypeArgument, file_id);
512 }
513 }
514
515 for lambda in &stub.lambda_targets {
517 let lambda_label = format!("{}::{}", lambda.owner_fqn, lambda.method_name);
518 let lambda_name_id = helper.intern(&lambda_label)?;
519 let lambda_fqn = format!("{}.lambda${}", stub.fqn, lambda.method_name);
520 let lambda_qname_id = helper.intern(&lambda_fqn)?;
521
522 let lambda_entry = NodeEntry::new(NodeKind::LambdaTarget, lambda_name_id, file_id)
523 .with_qualified_name(lambda_qname_id)
524 .with_definition(true);
525
526 let lambda_node_id = staging.add_node(lambda_entry);
527 record_node_ref(
528 &lambda_fqn,
529 lambda_node_id,
530 &jar_path,
531 file_id,
532 fqn_to_node,
533 fqn_to_nodes,
534 );
535 metadata_store.insert_typed(lambda_node_id, TypedMetadata::Classpath(cp_meta.clone()));
536
537 staging.add_edge(class_node_id, lambda_node_id, EdgeKind::Contains, file_id);
539 }
540
541 for inner in &stub.inner_classes {
543 if inner.outer_fqn.as_deref() == Some(&stub.fqn) {
546 }
552 }
553
554 if let Some(ref module) = stub.module {
556 let mod_name_id = helper.intern(&module.name)?;
557 let mod_fqn = format!("module:{}", module.name);
558 let mod_qname_id = helper.intern(&mod_fqn)?;
559
560 let mod_entry = NodeEntry::new(NodeKind::JavaModule, mod_name_id, file_id)
561 .with_qualified_name(mod_qname_id)
562 .with_definition(true);
563
564 let mod_node_id = staging.add_node(mod_entry);
565 record_node_ref(
566 &mod_fqn,
567 mod_node_id,
568 &jar_path,
569 file_id,
570 fqn_to_node,
571 fqn_to_nodes,
572 );
573 metadata_store.insert_typed(mod_node_id, TypedMetadata::Classpath(cp_meta.clone()));
574
575 staging.add_edge(class_node_id, mod_node_id, EdgeKind::Contains, file_id);
577 }
578
579 Ok(())
580}
581
582fn record_node_ref(
583 fqn: &str,
584 node_id: NodeId,
585 jar_path: &Path,
586 file_id: FileId,
587 fqn_to_node: &mut HashMap<String, NodeId>,
588 fqn_to_nodes: &mut HashMap<String, Vec<ClasspathNodeRef>>,
589) {
590 fqn_to_node.entry(fqn.to_owned()).or_insert(node_id);
591 fqn_to_nodes
592 .entry(fqn.to_owned())
593 .or_default()
594 .push(ClasspathNodeRef {
595 node_id,
596 fqn: fqn.to_owned(),
597 jar_path: jar_path.to_path_buf(),
598 file_id,
599 });
600}
601
602fn find_provenance_for_jar<'a>(
608 jar_path: &Path,
609 prov_map: &HashMap<&Path, &'a ClasspathProvenance>,
610) -> Option<&'a ClasspathProvenance> {
611 prov_map.get(jar_path).copied()
612}
613
614#[allow(clippy::too_many_lines)]
625#[allow(clippy::implicit_hasher)] pub fn create_classpath_edges(
627 #[allow(clippy::implicit_hasher)] index: &ClasspathIndex,
629 fqn_to_nodes: &HashMap<String, Vec<ClasspathNodeRef>>,
630 provenance: &[ClasspathProvenance],
631 staging: &mut StagingGraph,
632) {
633 let prov_map: HashMap<&Path, &ClasspathProvenance> = provenance
634 .iter()
635 .map(|p| (p.jar_path.as_path(), p))
636 .collect();
637
638 for stub in &index.classes {
639 let source_jar = stub.source_jar.as_deref().map(Path::new);
640 let Some(class_node) = select_node_ref(&stub.fqn, source_jar, fqn_to_nodes, &prov_map)
641 else {
642 continue;
643 };
644 let class_node_id = class_node.node_id;
645 let file_id = class_node.file_id;
646
647 if let Some(ref superclass_fqn) = stub.superclass
649 && superclass_fqn != "java.lang.Object"
650 {
651 if let Some(super_node) =
652 select_node_ref(superclass_fqn, source_jar, fqn_to_nodes, &prov_map)
653 {
654 let super_node_id = super_node.node_id;
655 staging.add_edge(class_node_id, super_node_id, EdgeKind::Inherits, file_id);
656 } else {
657 log::debug!(
658 "classpath: skipping Inherits edge for {} → {} (target not in graph)",
659 stub.fqn,
660 superclass_fqn
661 );
662 }
663 }
664
665 for iface_fqn in &stub.interfaces {
667 if let Some(iface_node) =
668 select_node_ref(iface_fqn, source_jar, fqn_to_nodes, &prov_map)
669 {
670 let iface_node_id = iface_node.node_id;
671 staging.add_edge(class_node_id, iface_node_id, EdgeKind::Implements, file_id);
672 } else {
673 log::debug!(
674 "classpath: skipping Implements edge for {} → {} (target not in graph)",
675 stub.fqn,
676 iface_fqn
677 );
678 }
679 }
680
681 if let Some(ref gen_sig) = stub.generic_signature {
683 for tp in &gen_sig.type_parameters {
684 let tp_fqn = format!("{}.<{}>", stub.fqn, tp.name);
685 let Some(tp_node) = select_node_ref(&tp_fqn, source_jar, fqn_to_nodes, &prov_map)
686 else {
687 continue;
688 };
689 let tp_node_id = tp_node.node_id;
690
691 if let Some(ref bound) = tp.class_bound
693 && let Some(bound_fqn) = extract_class_fqn_from_type_sig(bound)
694 && let Some(bound_node) =
695 select_node_ref(bound_fqn, source_jar, fqn_to_nodes, &prov_map)
696 {
697 let bound_node_id = bound_node.node_id;
698 staging.add_edge(tp_node_id, bound_node_id, EdgeKind::GenericBound, file_id);
699 }
700
701 for ibound in &tp.interface_bounds {
703 if let Some(bound_fqn) = extract_class_fqn_from_type_sig(ibound)
704 && let Some(bound_node) =
705 select_node_ref(bound_fqn, source_jar, fqn_to_nodes, &prov_map)
706 {
707 let bound_node_id = bound_node.node_id;
708 staging.add_edge(
709 tp_node_id,
710 bound_node_id,
711 EdgeKind::GenericBound,
712 file_id,
713 );
714 }
715 }
716 }
717 }
718
719 for ann in &stub.annotations {
721 if let Some(ann_type_node) =
722 select_node_ref(&ann.type_fqn, source_jar, fqn_to_nodes, &prov_map)
723 {
724 let ann_type_node_id = ann_type_node.node_id;
725 staging.add_edge(
726 class_node_id,
727 ann_type_node_id,
728 EdgeKind::AnnotatedWith,
729 file_id,
730 );
731 }
732 }
733
734 for method in &stub.methods {
736 let method_fqn = format!("{}.{}{}", stub.fqn, method.name, method.descriptor);
737 if let Some(method_node) =
738 select_node_ref(&method_fqn, source_jar, fqn_to_nodes, &prov_map)
739 {
740 let method_node_id = method_node.node_id;
741 for ann in &method.annotations {
742 if let Some(ann_type_node) =
743 select_node_ref(&ann.type_fqn, source_jar, fqn_to_nodes, &prov_map)
744 {
745 let ann_type_node_id = ann_type_node.node_id;
746 staging.add_edge(
747 method_node_id,
748 ann_type_node_id,
749 EdgeKind::AnnotatedWith,
750 file_id,
751 );
752 }
753 }
754 }
755 }
756
757 for field in &stub.fields {
759 let field_fqn = format!("{}.{}", stub.fqn, field.name);
760 if let Some(field_node) =
761 select_node_ref(&field_fqn, source_jar, fqn_to_nodes, &prov_map)
762 {
763 let field_node_id = field_node.node_id;
764 for ann in &field.annotations {
765 if let Some(ann_type_node) =
766 select_node_ref(&ann.type_fqn, source_jar, fqn_to_nodes, &prov_map)
767 {
768 let ann_type_node_id = ann_type_node.node_id;
769 staging.add_edge(
770 field_node_id,
771 ann_type_node_id,
772 EdgeKind::AnnotatedWith,
773 file_id,
774 );
775 }
776 }
777 }
778 }
779
780 for inner in &stub.inner_classes {
782 if inner.outer_fqn.as_deref() == Some(&stub.fqn)
783 && let Some(inner_node) =
784 select_node_ref(&inner.inner_fqn, source_jar, fqn_to_nodes, &prov_map)
785 {
786 let inner_node_id = inner_node.node_id;
787 staging.add_edge(class_node_id, inner_node_id, EdgeKind::Contains, file_id);
788 }
789 }
790
791 if let Some(ref module) = stub.module {
793 let mod_fqn = format!("module:{}", module.name);
794 let Some(mod_node) = select_node_ref(&mod_fqn, source_jar, fqn_to_nodes, &prov_map)
795 else {
796 continue;
797 };
798 let mod_node_id = mod_node.node_id;
799
800 for req in &module.requires {
802 let req_mod_fqn = format!("module:{}", req.module_name);
803 if let Some(req_node) =
804 select_node_ref(&req_mod_fqn, source_jar, fqn_to_nodes, &prov_map)
805 {
806 let req_node_id = req_node.node_id;
807 staging.add_edge(mod_node_id, req_node_id, EdgeKind::ModuleRequires, file_id);
808 }
809 }
810
811 for exp in &module.exports {
813 let pkg_classes = index.lookup_package(&exp.package);
815 for pkg_class in pkg_classes {
816 for pkg_class_node in
817 select_node_refs(&pkg_class.fqn, source_jar, fqn_to_nodes, &prov_map)
818 {
819 let pkg_class_node_id = pkg_class_node.node_id;
820 staging.add_edge(
821 mod_node_id,
822 pkg_class_node_id,
823 EdgeKind::ModuleExports,
824 file_id,
825 );
826 }
827 }
828 }
829
830 for opens in &module.opens {
832 let pkg_classes = index.lookup_package(&opens.package);
833 for pkg_class in pkg_classes {
834 for pkg_class_node in
835 select_node_refs(&pkg_class.fqn, source_jar, fqn_to_nodes, &prov_map)
836 {
837 let pkg_class_node_id = pkg_class_node.node_id;
838 staging.add_edge(
839 mod_node_id,
840 pkg_class_node_id,
841 EdgeKind::ModuleOpens,
842 file_id,
843 );
844 }
845 }
846 }
847
848 for provides in &module.provides {
850 for impl_fqn in &provides.implementations {
851 if let Some(impl_node) =
852 select_node_ref(impl_fqn, source_jar, fqn_to_nodes, &prov_map)
853 {
854 let impl_node_id = impl_node.node_id;
855 staging.add_edge(
856 mod_node_id,
857 impl_node_id,
858 EdgeKind::ModuleProvides,
859 file_id,
860 );
861 }
862 }
863 }
864 }
865 }
866}
867
868fn select_node_ref<'a>(
869 fqn: &str,
870 source_jar: Option<&Path>,
871 fqn_to_nodes: &'a HashMap<String, Vec<ClasspathNodeRef>>,
872 prov_map: &HashMap<&Path, &ClasspathProvenance>,
873) -> Option<&'a ClasspathNodeRef> {
874 let candidates = select_node_refs(fqn, source_jar, fqn_to_nodes, prov_map);
875 candidates.first().copied()
876}
877
878fn select_node_refs<'a>(
879 fqn: &str,
880 source_jar: Option<&Path>,
881 fqn_to_nodes: &'a HashMap<String, Vec<ClasspathNodeRef>>,
882 prov_map: &HashMap<&Path, &ClasspathProvenance>,
883) -> Vec<&'a ClasspathNodeRef> {
884 let Some(candidates) = fqn_to_nodes.get(fqn) else {
885 return Vec::new();
886 };
887
888 let Some(source_jar) = source_jar else {
889 return candidates.iter().collect();
890 };
891
892 if let Some(exact_match) = candidates
893 .iter()
894 .find(|candidate| candidate.jar_path.as_path() == source_jar)
895 {
896 return vec![exact_match];
897 }
898
899 let scoped: Vec<_> = candidates
900 .iter()
901 .filter(|candidate| jars_share_scope(source_jar, candidate.jar_path.as_path(), prov_map))
902 .collect();
903 if scoped.is_empty() {
904 candidates.iter().collect()
905 } else {
906 scoped
907 }
908}
909
910fn jars_share_scope(
911 source_jar: &Path,
912 target_jar: &Path,
913 prov_map: &HashMap<&Path, &ClasspathProvenance>,
914) -> bool {
915 if source_jar == target_jar {
916 return true;
917 }
918
919 let Some(source) = prov_map.get(source_jar) else {
920 debug!(
921 "classpath: provenance missing for source jar {}; allowing scope fallback",
922 source_jar.display()
923 );
924 return true;
925 };
926 let Some(target) = prov_map.get(target_jar) else {
927 debug!(
928 "classpath: provenance missing for target jar {}; allowing scope fallback",
929 target_jar.display()
930 );
931 return true;
932 };
933
934 source.scopes.iter().any(|source_scope| {
935 target
936 .scopes
937 .iter()
938 .any(|target_scope| source_scope.module_root == target_scope.module_root)
939 })
940}
941
942fn extract_class_fqn_from_type_sig(sig: &crate::stub::model::TypeSignature) -> Option<&str> {
944 match sig {
945 crate::stub::model::TypeSignature::Class { fqn, .. } => Some(fqn.as_str()),
946 _ => None,
947 }
948}
949
950#[cfg(test)]
955mod tests {
956 use super::*;
957 use crate::stub::model::{
958 AccessFlags, AnnotationStub, ClassKind, FieldStub, GenericClassSignature, InnerClassEntry,
959 LambdaTargetStub, MethodStub, ModuleExports, ModuleProvides, ModuleRequires, ModuleStub,
960 ReferenceKind, TypeParameterStub, TypeSignature,
961 };
962 use sqry_core::graph::unified::BidirectionalEdgeStore;
963 use sqry_core::graph::unified::storage::AuxiliaryIndices;
964 use sqry_core::graph::unified::storage::NodeArena;
965
966 fn make_interner() -> StringInterner {
971 StringInterner::new()
972 }
973
974 fn make_staging() -> StagingGraph {
975 StagingGraph::default()
976 }
977
978 fn make_provenance(jar: &str, direct: bool) -> ClasspathProvenance {
979 ClasspathProvenance {
980 jar_path: PathBuf::from(jar),
981 coordinates: Some(format!(
982 "group:artifact:{}",
983 if direct { "1.0" } else { "2.0" }
984 )),
985 is_direct: direct,
986 scopes: vec![crate::graph::provenance::ClasspathScope {
987 module_name: "test".to_owned(),
988 module_root: PathBuf::from("/repo/test"),
989 is_direct: direct,
990 }],
991 }
992 }
993
994 fn make_stub(fqn: &str) -> ClassStub {
995 ClassStub {
996 fqn: fqn.to_owned(),
997 name: fqn.rsplit('.').next().unwrap_or(fqn).to_owned(),
998 kind: ClassKind::Class,
999 access: AccessFlags::new(AccessFlags::ACC_PUBLIC),
1000 superclass: Some("java.lang.Object".to_owned()),
1001 interfaces: vec![],
1002 methods: vec![],
1003 fields: vec![],
1004 annotations: vec![],
1005 generic_signature: None,
1006 inner_classes: vec![],
1007 lambda_targets: vec![],
1008 module: None,
1009 record_components: vec![],
1010 enum_constants: vec![],
1011 source_file: None,
1012 source_jar: None,
1013 kotlin_metadata: None,
1014 scala_signature: None,
1015 }
1016 }
1017
1018 fn make_method(name: &str) -> MethodStub {
1019 MethodStub {
1020 name: name.to_owned(),
1021 access: AccessFlags::new(AccessFlags::ACC_PUBLIC),
1022 descriptor: "()V".to_owned(),
1023 generic_signature: None,
1024 annotations: vec![],
1025 parameter_annotations: vec![],
1026 parameter_names: vec![],
1027 return_type: TypeSignature::Base(crate::stub::model::BaseType::Void),
1028 parameter_types: vec![],
1029 }
1030 }
1031
1032 fn make_field(name: &str) -> FieldStub {
1033 FieldStub {
1034 name: name.to_owned(),
1035 access: AccessFlags::new(AccessFlags::ACC_PRIVATE),
1036 descriptor: "I".to_owned(),
1037 generic_signature: None,
1038 annotations: vec![],
1039 constant_value: None,
1040 }
1041 }
1042
1043 fn run_emission(
1045 stubs: Vec<ClassStub>,
1046 provenance: &[ClasspathProvenance],
1047 ) -> (
1048 EmissionResult,
1049 StagingGraph,
1050 FileRegistry,
1051 StringInterner,
1052 NodeMetadataStore,
1053 ) {
1054 let default_jar = provenance
1055 .first()
1056 .map(|entry| entry.jar_path.display().to_string());
1057 let normalized_stubs = stubs
1058 .into_iter()
1059 .map(|mut stub| {
1060 if stub.source_jar.is_none() {
1061 stub.source_jar = default_jar.clone();
1062 }
1063 stub
1064 })
1065 .collect();
1066 let index = ClasspathIndex::build(normalized_stubs);
1067 let mut staging = make_staging();
1068 let mut file_registry = FileRegistry::new();
1069 let mut interner = make_interner();
1070 let mut metadata_store = NodeMetadataStore::new();
1071
1072 let result = emit_classpath_nodes(
1073 &index,
1074 &mut staging,
1075 &mut file_registry,
1076 &mut interner,
1077 &mut metadata_store,
1078 provenance,
1079 )
1080 .expect("emission should succeed");
1081
1082 (result, staging, file_registry, interner, metadata_store)
1083 }
1084
1085 #[test]
1090 fn test_simple_class_emits_nodes() {
1091 let mut stub = make_stub("com.example.Foo");
1092 stub.methods = vec![make_method("bar"), make_method("baz")];
1093 stub.fields = vec![make_field("count")];
1094
1095 let prov = vec![make_provenance("/jars/example.jar", true)];
1096 let (result, _staging, _registry, _interner, _meta) = run_emission(vec![stub], &prov);
1097
1098 assert!(
1100 result.fqn_to_node.contains_key("com.example.Foo"),
1101 "class node should be emitted"
1102 );
1103
1104 assert!(
1106 result.fqn_to_node.contains_key("com.example.Foo.bar()V"),
1107 "method 'bar' should be emitted"
1108 );
1109 assert!(
1110 result.fqn_to_node.contains_key("com.example.Foo.baz()V"),
1111 "method 'baz' should be emitted"
1112 );
1113
1114 assert!(
1116 result.fqn_to_node.contains_key("com.example.Foo.count"),
1117 "field 'count' should be emitted"
1118 );
1119
1120 assert_eq!(result.fqn_to_node.len(), 4);
1122 }
1123
1124 #[test]
1129 fn test_inheritance_edge_created() {
1130 let mut list = make_stub("java.util.AbstractList");
1131 list.superclass = Some("java.util.AbstractCollection".to_owned());
1132
1133 let collection = make_stub("java.util.AbstractCollection");
1134
1135 let prov = vec![make_provenance("/jars/rt.jar", true)];
1136 let stubs = vec![list, collection];
1137 let index = ClasspathIndex::build(stubs.clone());
1138 let (result, mut staging, _registry, _interner, _meta) = run_emission(stubs, &prov);
1139
1140 create_classpath_edges(&index, &result.fqn_to_nodes, &prov, &mut staging);
1141
1142 assert!(result.fqn_to_node.contains_key("java.util.AbstractList"));
1144 assert!(
1145 result
1146 .fqn_to_node
1147 .contains_key("java.util.AbstractCollection")
1148 );
1149
1150 let stats = staging.stats();
1152 assert!(
1154 stats.edges_staged > 0,
1155 "should have at least one edge staged"
1156 );
1157 }
1158
1159 #[test]
1164 fn test_implements_edge_created() {
1165 let mut array_list = make_stub("java.util.ArrayList");
1166 array_list.interfaces = vec![
1167 "java.util.List".to_owned(),
1168 "java.io.Serializable".to_owned(),
1169 ];
1170
1171 let list_iface = {
1172 let mut s = make_stub("java.util.List");
1173 s.kind = ClassKind::Interface;
1174 s
1175 };
1176
1177 let serializable = {
1178 let mut s = make_stub("java.io.Serializable");
1179 s.kind = ClassKind::Interface;
1180 s
1181 };
1182
1183 let prov = vec![make_provenance("/jars/rt.jar", true)];
1184 let stubs = vec![array_list, list_iface, serializable];
1185 let index = ClasspathIndex::build(stubs.clone());
1186 let (result, mut staging, _registry, _interner, _meta) = run_emission(stubs, &prov);
1187
1188 create_classpath_edges(&index, &result.fqn_to_nodes, &prov, &mut staging);
1189
1190 assert!(result.fqn_to_node.contains_key("java.util.ArrayList"));
1192 assert!(result.fqn_to_node.contains_key("java.util.List"));
1193 assert!(result.fqn_to_node.contains_key("java.io.Serializable"));
1194 }
1195
1196 #[test]
1197 fn test_emit_into_code_graph_commits_edges_and_metadata() {
1198 let mut child = make_stub("java.util.AbstractList");
1199 child.superclass = Some("java.util.AbstractCollection".to_owned());
1200 child.source_jar = Some("/jars/rt.jar".to_owned());
1201
1202 let mut parent = make_stub("java.util.AbstractCollection");
1203 parent.source_jar = Some("/jars/rt.jar".to_owned());
1204
1205 let index = ClasspathIndex::build(vec![child, parent]);
1206 let provenance = vec![make_provenance("/jars/rt.jar", true)];
1207 let mut graph = CodeGraph::new();
1208
1209 let result = emit_into_code_graph(&index, &mut graph, &provenance)
1210 .expect("in-place classpath emission should succeed");
1211
1212 let child_id = *result
1213 .fqn_to_node
1214 .get("java.util.AbstractList")
1215 .expect("child class node should be returned");
1216 let parent_id = *result
1217 .fqn_to_node
1218 .get("java.util.AbstractCollection")
1219 .expect("parent class node should be returned");
1220
1221 assert!(
1222 graph.nodes().get(child_id).is_some(),
1223 "child node should exist"
1224 );
1225 assert!(
1226 graph.edge_count() > 0,
1227 "structural classpath edges should be committed"
1228 );
1229 assert!(
1230 graph.edges().edges_from(child_id).iter().any(|edge| {
1231 matches!(edge.kind, EdgeKind::Inherits) && edge.target == parent_id
1232 }),
1233 "child class should have an inherits edge to its parent"
1234 );
1235 assert!(
1236 matches!(
1237 graph.macro_metadata().get_typed(child_id),
1238 Some(TypedMetadata::Classpath(_))
1239 ),
1240 "classpath metadata should remain attached after node-id remap"
1241 );
1242 }
1243
1244 #[test]
1245 fn test_emit_into_code_graph_preserves_graph_on_failure() {
1246 let mut strings = StringInterner::with_max_ids(2);
1247 let existing_name = strings.intern("existing").unwrap();
1248 let existing_qname = strings.intern("existing::node").unwrap();
1249
1250 let mut files = FileRegistry::new();
1251 let file_id = files.register(Path::new("/existing.rs")).unwrap();
1252
1253 let mut nodes = NodeArena::new();
1254 let existing_node = nodes
1255 .alloc(
1256 NodeEntry::new(NodeKind::Function, existing_name, file_id)
1257 .with_qualified_name(existing_qname),
1258 )
1259 .unwrap();
1260
1261 let mut graph = CodeGraph::from_components(
1262 nodes,
1263 BidirectionalEdgeStore::new(),
1264 strings,
1265 files,
1266 AuxiliaryIndices::new(),
1267 NodeMetadataStore::new(),
1268 );
1269 graph.macro_metadata_mut().insert_typed(
1270 existing_node,
1271 TypedMetadata::Classpath(ClasspathNodeMetadata {
1272 coordinates: Some("group:existing:1.0".to_owned()),
1273 jar_path: "/existing.jar".to_owned(),
1274 fqn: "existing::node".to_owned(),
1275 is_direct_dependency: true,
1276 }),
1277 );
1278
1279 let snapshot_before = graph.snapshot();
1280 let existing_path_before = snapshot_before
1281 .files()
1282 .resolve(file_id)
1283 .expect("existing file should resolve")
1284 .to_path_buf();
1285 let existing_meta_before = snapshot_before
1286 .macro_metadata()
1287 .get_typed(existing_node)
1288 .cloned();
1289
1290 let mut failing_stub = make_stub("com.example.WillFail");
1291 failing_stub.source_jar = Some("/jars/failing.jar".to_owned());
1292 let index = ClasspathIndex::build(vec![failing_stub]);
1293 let provenance = vec![make_provenance("/jars/failing.jar", true)];
1294
1295 let error = emit_into_code_graph(&index, &mut graph, &provenance)
1296 .expect_err("emission should fail when the cloned interner is full");
1297 assert!(
1298 error.to_string().contains("string intern failed"),
1299 "unexpected error: {error}"
1300 );
1301
1302 assert_eq!(graph.node_count(), snapshot_before.nodes().len());
1303 assert_eq!(graph.edge_count(), 0);
1304 assert_eq!(graph.strings().len(), snapshot_before.strings().len());
1305 assert_eq!(graph.files().len(), snapshot_before.files().len());
1306
1307 let surviving_node = graph
1308 .nodes()
1309 .get(existing_node)
1310 .expect("existing node must survive failed emission");
1311 assert_eq!(surviving_node.name, existing_name);
1312 assert_eq!(
1313 graph
1314 .files()
1315 .resolve(file_id)
1316 .map(|path| path.to_path_buf()),
1317 Some(existing_path_before)
1318 );
1319 assert_eq!(
1320 graph.macro_metadata().get_typed(existing_node),
1321 existing_meta_before.as_ref()
1322 );
1323 }
1324
1325 #[test]
1330 fn test_classpath_metadata_attached() {
1331 let stub = make_stub("com.google.common.collect.ImmutableList");
1332 let prov = vec![ClasspathProvenance {
1333 jar_path: PathBuf::from("/home/user/.m2/repository/guava-33.0.0.jar"),
1334 coordinates: Some("com.google.guava:guava:33.0.0".to_owned()),
1335 is_direct: true,
1336 scopes: vec![crate::graph::provenance::ClasspathScope {
1337 module_name: "test".to_owned(),
1338 module_root: PathBuf::from("/repo/test"),
1339 is_direct: true,
1340 }],
1341 }];
1342
1343 let (result, _staging, _registry, _interner, metadata_store) =
1344 run_emission(vec![stub], &prov);
1345
1346 let node_id = result
1347 .fqn_to_node
1348 .get("com.google.common.collect.ImmutableList")
1349 .expect("node should exist");
1350
1351 let metadata = metadata_store
1352 .get_typed(*node_id)
1353 .expect("metadata should be attached");
1354
1355 match metadata {
1356 TypedMetadata::Classpath(cp) => {
1357 assert_eq!(cp.fqn, "com.google.common.collect.ImmutableList");
1358 assert_eq!(
1359 cp.coordinates.as_deref(),
1360 Some("com.google.guava:guava:33.0.0")
1361 );
1362 assert!(cp.is_direct_dependency);
1363 assert!(cp.jar_path.contains("guava-33.0.0.jar"));
1364 }
1365 TypedMetadata::Macro(_) => panic!("expected Classpath metadata, got Macro"),
1366 }
1367 }
1368
1369 #[test]
1374 fn test_zero_spans_on_classpath_nodes() {
1375 let mut stub = make_stub("com.example.ZeroSpan");
1376 stub.methods = vec![make_method("doWork")];
1377
1378 let prov = vec![make_provenance("/jars/test.jar", true)];
1379 let (result, staging, _registry, _interner, _meta) = run_emission(vec![stub], &prov);
1380
1381 assert!(
1385 !result.fqn_to_node.is_empty(),
1386 "should have emitted at least one node"
1387 );
1388
1389 let stats = staging.stats();
1391 assert!(
1392 stats.nodes_staged >= 2,
1393 "should have at least 2 nodes (class + method)"
1394 );
1395 }
1396
1397 #[test]
1402 fn test_annotation_edges() {
1403 let mut stub = make_stub("com.example.MyService");
1404 stub.annotations = vec![AnnotationStub {
1405 type_fqn: "org.springframework.stereotype.Service".to_owned(),
1406 elements: vec![],
1407 is_runtime_visible: true,
1408 }];
1409
1410 let ann_type = {
1412 let mut s = make_stub("org.springframework.stereotype.Service");
1413 s.kind = ClassKind::Annotation;
1414 s
1415 };
1416
1417 let prov = vec![make_provenance("/jars/app.jar", true)];
1418 let stubs = vec![stub, ann_type];
1419 let index = ClasspathIndex::build(stubs.clone());
1420 let (result, mut staging, _registry, _interner, _meta) = run_emission(stubs, &prov);
1421
1422 create_classpath_edges(&index, &result.fqn_to_nodes, &prov, &mut staging);
1423
1424 assert!(result.fqn_to_node.contains_key("com.example.MyService"));
1426 assert!(
1427 result
1428 .fqn_to_node
1429 .contains_key("org.springframework.stereotype.Service")
1430 );
1431
1432 let stats = staging.stats();
1434 assert!(
1435 stats.edges_staged > 0,
1436 "should have annotation edges staged"
1437 );
1438 }
1439
1440 #[test]
1445 fn test_module_edges() {
1446 let provider_class = make_stub("com.example.spi.MyProvider");
1447 let exported_class = make_stub("com.example.api.MyApi");
1448
1449 let mut module_stub = make_stub("module-info");
1450 module_stub.kind = ClassKind::Module;
1451 module_stub.module = Some(ModuleStub {
1452 name: "com.example".to_owned(),
1453 access: AccessFlags::new(0),
1454 version: None,
1455 requires: vec![ModuleRequires {
1456 module_name: "java.base".to_owned(),
1457 access: AccessFlags::new(0),
1458 version: None,
1459 }],
1460 exports: vec![ModuleExports {
1461 package: "com.example.api".to_owned(),
1462 access: AccessFlags::new(0),
1463 to_modules: vec![],
1464 }],
1465 opens: vec![],
1466 provides: vec![ModuleProvides {
1467 service: "com.example.spi.SomeService".to_owned(),
1468 implementations: vec!["com.example.spi.MyProvider".to_owned()],
1469 }],
1470 uses: vec![],
1471 });
1472
1473 let prov = vec![make_provenance("/jars/example.jar", true)];
1474 let stubs = vec![module_stub, provider_class, exported_class];
1475 let index = ClasspathIndex::build(stubs.clone());
1476 let (result, mut staging, _registry, _interner, _meta) = run_emission(stubs, &prov);
1477
1478 create_classpath_edges(&index, &result.fqn_to_nodes, &prov, &mut staging);
1479
1480 assert!(
1482 result.fqn_to_node.contains_key("module:com.example"),
1483 "module node should be emitted"
1484 );
1485
1486 let stats = staging.stats();
1487 assert!(stats.edges_staged > 0, "should have module edges staged");
1488 }
1489
1490 #[test]
1495 fn test_enum_constants_emitted() {
1496 let mut stub = make_stub("java.time.DayOfWeek");
1497 stub.kind = ClassKind::Enum;
1498 stub.enum_constants = vec![
1499 "MONDAY".to_owned(),
1500 "TUESDAY".to_owned(),
1501 "WEDNESDAY".to_owned(),
1502 ];
1503
1504 let prov = vec![make_provenance("/jars/rt.jar", true)];
1505 let (result, _staging, _registry, _interner, _meta) = run_emission(vec![stub], &prov);
1506
1507 assert!(
1508 result
1509 .fqn_to_node
1510 .contains_key("java.time.DayOfWeek.MONDAY")
1511 );
1512 assert!(
1513 result
1514 .fqn_to_node
1515 .contains_key("java.time.DayOfWeek.TUESDAY")
1516 );
1517 assert!(
1518 result
1519 .fqn_to_node
1520 .contains_key("java.time.DayOfWeek.WEDNESDAY")
1521 );
1522 }
1523
1524 #[test]
1529 fn test_type_parameters_emitted() {
1530 let mut stub = make_stub("java.util.HashMap");
1531 stub.generic_signature = Some(GenericClassSignature {
1532 type_parameters: vec![
1533 TypeParameterStub {
1534 name: "K".to_owned(),
1535 class_bound: None,
1536 interface_bounds: vec![],
1537 },
1538 TypeParameterStub {
1539 name: "V".to_owned(),
1540 class_bound: None,
1541 interface_bounds: vec![],
1542 },
1543 ],
1544 superclass: TypeSignature::Class {
1545 fqn: "java.util.AbstractMap".to_owned(),
1546 type_arguments: vec![],
1547 },
1548 interfaces: vec![],
1549 });
1550
1551 let prov = vec![make_provenance("/jars/rt.jar", true)];
1552 let (result, _staging, _registry, _interner, _meta) = run_emission(vec![stub], &prov);
1553
1554 assert!(result.fqn_to_node.contains_key("java.util.HashMap.<K>"));
1555 assert!(result.fqn_to_node.contains_key("java.util.HashMap.<V>"));
1556 }
1557
1558 #[test]
1563 fn test_generic_bound_edges() {
1564 let comparable = make_stub("java.lang.Comparable");
1565
1566 let mut stub = make_stub("com.example.Sorted");
1567 stub.generic_signature = Some(GenericClassSignature {
1568 type_parameters: vec![TypeParameterStub {
1569 name: "T".to_owned(),
1570 class_bound: Some(TypeSignature::Class {
1571 fqn: "java.lang.Comparable".to_owned(),
1572 type_arguments: vec![],
1573 }),
1574 interface_bounds: vec![],
1575 }],
1576 superclass: TypeSignature::Class {
1577 fqn: "java.lang.Object".to_owned(),
1578 type_arguments: vec![],
1579 },
1580 interfaces: vec![],
1581 });
1582
1583 let prov = vec![make_provenance("/jars/rt.jar", true)];
1584 let stubs = vec![stub, comparable];
1585 let index = ClasspathIndex::build(stubs.clone());
1586 let (result, mut staging, _registry, _interner, _meta) = run_emission(stubs, &prov);
1587
1588 create_classpath_edges(&index, &result.fqn_to_nodes, &prov, &mut staging);
1589
1590 assert!(result.fqn_to_node.contains_key("com.example.Sorted.<T>"));
1592 assert!(result.fqn_to_node.contains_key("java.lang.Comparable"));
1593 }
1594
1595 #[test]
1600 fn test_lambda_targets_emitted() {
1601 let mut stub = make_stub("com.example.Processor");
1602 stub.lambda_targets = vec![LambdaTargetStub {
1603 owner_fqn: "java.lang.String".to_owned(),
1604 method_name: "toUpperCase".to_owned(),
1605 method_descriptor: "()Ljava/lang/String;".to_owned(),
1606 reference_kind: ReferenceKind::InvokeVirtual,
1607 }];
1608
1609 let prov = vec![make_provenance("/jars/app.jar", true)];
1610 let (result, _staging, _registry, _interner, _meta) = run_emission(vec![stub], &prov);
1611
1612 assert!(
1613 result
1614 .fqn_to_node
1615 .contains_key("com.example.Processor.lambda$toUpperCase")
1616 );
1617 }
1618
1619 #[test]
1624 fn test_inner_class_contains_edge() {
1625 let outer = {
1626 let mut s = make_stub("com.example.Outer");
1627 s.inner_classes = vec![InnerClassEntry {
1628 inner_fqn: "com.example.Outer.Inner".to_owned(),
1629 outer_fqn: Some("com.example.Outer".to_owned()),
1630 inner_name: Some("Inner".to_owned()),
1631 access: AccessFlags::new(AccessFlags::ACC_PUBLIC),
1632 }];
1633 s
1634 };
1635
1636 let inner = make_stub("com.example.Outer.Inner");
1637
1638 let prov = vec![make_provenance("/jars/app.jar", true)];
1639 let stubs = vec![outer, inner];
1640 let index = ClasspathIndex::build(stubs.clone());
1641 let (result, mut staging, _registry, _interner, _meta) = run_emission(stubs, &prov);
1642
1643 create_classpath_edges(&index, &result.fqn_to_nodes, &prov, &mut staging);
1644
1645 assert!(result.fqn_to_node.contains_key("com.example.Outer"));
1646 assert!(result.fqn_to_node.contains_key("com.example.Outer.Inner"));
1647 }
1648
1649 #[test]
1654 fn test_empty_index_no_nodes() {
1655 let prov: Vec<ClasspathProvenance> = vec![];
1656 let (result, staging, _registry, _interner, _meta) = run_emission(vec![], &prov);
1657
1658 assert!(result.fqn_to_node.is_empty());
1659 assert_eq!(staging.stats().nodes_staged, 0);
1660 }
1661
1662 #[test]
1667 fn test_synthetic_file_path_convention() {
1668 let stub = make_stub("com.example.Foo");
1669 let prov = vec![make_provenance("/jars/example.jar", true)];
1670 let (result, _staging, file_registry, _interner, _meta) = run_emission(vec![stub], &prov);
1671
1672 let file_id = result
1673 .file_id_map
1674 .get("com.example.Foo")
1675 .expect("file ID should exist");
1676
1677 let path = file_registry
1678 .resolve(*file_id)
1679 .expect("file should be resolvable");
1680
1681 let path_str = path.to_string_lossy();
1682 assert!(
1683 path_str.contains("!/com/example/Foo.class"),
1684 "synthetic path should follow JAR convention, got: {path_str}"
1685 );
1686 }
1687
1688 #[test]
1693 fn test_interface_kind_mapping() {
1694 let mut stub = make_stub("java.util.List");
1695 stub.kind = ClassKind::Interface;
1696
1697 let prov = vec![make_provenance("/jars/rt.jar", true)];
1698 let (result, _staging, _registry, _interner, _meta) = run_emission(vec![stub], &prov);
1699
1700 assert!(result.fqn_to_node.contains_key("java.util.List"));
1701 }
1702
1703 #[test]
1708 fn test_method_level_annotation_edge() {
1709 let override_ann = {
1710 let mut s = make_stub("java.lang.Override");
1711 s.kind = ClassKind::Annotation;
1712 s
1713 };
1714
1715 let mut stub = make_stub("com.example.Foo");
1716 stub.methods = vec![MethodStub {
1717 name: "toString".to_owned(),
1718 access: AccessFlags::new(AccessFlags::ACC_PUBLIC),
1719 descriptor: "()Ljava/lang/String;".to_owned(),
1720 generic_signature: None,
1721 annotations: vec![AnnotationStub {
1722 type_fqn: "java.lang.Override".to_owned(),
1723 elements: vec![],
1724 is_runtime_visible: true,
1725 }],
1726 parameter_annotations: vec![],
1727 parameter_names: vec![],
1728 return_type: TypeSignature::Base(crate::stub::model::BaseType::Void),
1729 parameter_types: vec![],
1730 }];
1731
1732 let prov = vec![make_provenance("/jars/rt.jar", true)];
1733 let stubs = vec![stub, override_ann];
1734 let index = ClasspathIndex::build(stubs.clone());
1735 let (result, mut staging, _registry, _interner, _meta) = run_emission(stubs, &prov);
1736
1737 create_classpath_edges(&index, &result.fqn_to_nodes, &prov, &mut staging);
1738
1739 assert!(
1740 result
1741 .fqn_to_node
1742 .contains_key("com.example.Foo.toString()Ljava/lang/String;")
1743 );
1744 assert!(result.fqn_to_node.contains_key("java.lang.Override"));
1745 }
1746
1747 #[test]
1752 fn test_no_provenance_still_emits() {
1753 let stub = make_stub("com.example.NoProv");
1754 let (result, _staging, _registry, _interner, _meta) = run_emission(vec![stub], &[]);
1755
1756 assert!(result.fqn_to_node.contains_key("com.example.NoProv"));
1757 }
1758
1759 #[test]
1764 fn test_visibility_mapping() {
1765 assert_eq!(
1766 access_to_visibility(&AccessFlags::new(AccessFlags::ACC_PUBLIC)),
1767 "public"
1768 );
1769 assert_eq!(
1770 access_to_visibility(&AccessFlags::new(AccessFlags::ACC_PROTECTED)),
1771 "protected"
1772 );
1773 assert_eq!(
1774 access_to_visibility(&AccessFlags::new(AccessFlags::ACC_PRIVATE)),
1775 "private"
1776 );
1777 assert_eq!(access_to_visibility(&AccessFlags::empty()), "package");
1778 }
1779}