Skip to main content

sqry_classpath/graph/
emitter.rs

1//! Emit classpath nodes and edges into the sqry unified graph.
2//!
3//! This module takes a [`ClasspathIndex`] (produced by bytecode parsing) and
4//! emits synthetic graph nodes and edges into a [`StagingGraph`]. Each JAR gets
5//! a synthetic [`FileId`] via [`FileRegistry::register_external()`], and each
6//! class/method/field becomes a graph node with zero-span metadata (since the
7//! data comes from bytecode, not source).
8//!
9//! ## Emission pipeline
10//!
11//! 1. **File registration** - For each unique JAR path, register a synthetic
12//!    `FileEntry` with path convention `{jar_path}!/{fqn}.class`.
13//! 2. **Node emission** - For each `ClassStub`, emit nodes for the class itself,
14//!    its methods, fields, annotations, type parameters, enum constants, lambda
15//!    targets, and module declarations.
16//! 3. **Structural edges** - Class→Method (`Defines`), Class→Field (`Defines`),
17//!    Class→InnerClass (`Contains`).
18//! 4. **Metadata attachment** - `ClasspathNodeMetadata` on all emitted nodes
19//!    (class, method, field, enum constant, type parameter, lambda target,
20//!    module).
21//!
22//! After emission, the returned `FQN→NodeId` mapping is used by
23//! [`create_classpath_edges`] for cross-reference edge creation.
24
25use 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
47// ---------------------------------------------------------------------------
48// String interning helper
49// ---------------------------------------------------------------------------
50
51/// Helper to intern strings via `StringInterner` and track the resulting IDs.
52///
53/// Unlike `GraphBuildHelper` which uses staging-local IDs, the classpath emitter
54/// interns directly into the `StringInterner` because classpath emission happens
55/// as a discrete phase before the per-file build pipeline. The resulting global
56/// `StringId`s are used directly in `NodeEntry` fields.
57struct 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    /// Intern a string, returning a global `StringId`.
71    ///
72    /// Caches results to avoid repeated lookups for common strings like
73    /// visibility modifiers.
74    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// ---------------------------------------------------------------------------
87// Visibility mapping
88// ---------------------------------------------------------------------------
89
90/// Map JVM access flags to a visibility string for the graph.
91#[allow(clippy::trivially_copy_pass_by_ref)] // API consistency with other methods
92fn 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
104/// Map `ClassKind` to the corresponding `NodeKind`.
105fn 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
115// ---------------------------------------------------------------------------
116// File ID management
117// ---------------------------------------------------------------------------
118
119/// Register a synthetic external file for a class within a JAR.
120///
121/// Path convention: `{jar_path}!/{fqn}.class` (similar to Java URL conventions
122/// for JAR entries like `jar:file:///path.jar!/com/example/Foo.class`).
123fn 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// ---------------------------------------------------------------------------
141// Node emission
142// ---------------------------------------------------------------------------
143
144/// Emit all classpath nodes and edges into a staging graph.
145///
146/// Returns the mapping of FQN to `NodeId` for cross-reference edge creation.
147///
148/// # Arguments
149///
150/// * `index` - The merged classpath index containing all parsed class stubs.
151/// * `staging` - The staging graph to emit nodes and edges into.
152/// * `file_registry` - File registry for synthetic file registration.
153/// * `interner` - String interner for name/qualifier interning.
154/// * `metadata_store` - Metadata store for classpath provenance attachment.
155/// * `provenance` - Provenance information for each JAR.
156///
157/// # Errors
158///
159/// Returns `ClasspathError::EmissionError` if string interning or file
160/// registration fails.
161#[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    // Build a lookup from JAR path to provenance for O(1) access.
177    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/// Duplicate-aware emitted node reference.
204#[derive(Debug, Clone)]
205pub struct ClasspathNodeRef {
206    /// Graph node id for the emitted symbol.
207    pub node_id: NodeId,
208    /// Fully qualified name for duplicate-aware disambiguation.
209    pub fqn: String,
210    /// JAR path the symbol originated from.
211    pub jar_path: PathBuf,
212    /// Synthetic file id registered for this emitted symbol.
213    pub file_id: FileId,
214}
215
216/// Result of classpath node emission.
217#[derive(Debug)]
218pub struct EmissionResult {
219    /// Mapping from fully qualified class name to its graph `NodeId`.
220    pub fqn_to_node: HashMap<String, NodeId>,
221    /// Duplicate-aware mapping from fully qualified name to emitted node refs.
222    pub fqn_to_nodes: HashMap<String, Vec<ClasspathNodeRef>>,
223    /// Mapping from fully qualified class name to its synthetic `FileId`.
224    pub file_id_map: HashMap<String, FileId>,
225}
226
227/// Emit classpath nodes and structural edges directly into a [`CodeGraph`].
228///
229/// This mutates the graph in place so existing graph state such as confidence
230/// and epoch metadata is preserved. It commits staged nodes into the node
231/// arena, remaps staged edges, inserts them into the bidirectional edge store,
232/// remaps metadata keys, and returns the final classpath `FQN -> NodeId` map.
233///
234/// # Errors
235///
236/// Returns [`ClasspathError::EmissionError`] if staging commit, string
237/// interning, or file registration fails.
238pub fn emit_into_code_graph(
239    index: &ClasspathIndex,
240    graph: &mut CodeGraph,
241    provenance: &[ClasspathProvenance],
242) -> ClasspathResult<EmissionResult> {
243    // Work against local clones first so a fallible emission path cannot leave
244    // the caller's graph partially emptied on early return.
245    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        // Classpath/JVM stub nodes never carry a shape descriptor (those are
309        // computed only for tree-sitter function/method bodies during language
310        // staging, never on the bytecode-stub path), so this map is empty in
311        // practice. Remap and carry it through anyway so the rebuild below can
312        // never silently strand a descriptor if that ever changes.
313        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/// Emit a single class stub and all its members into the staging graph.
344#[allow(clippy::too_many_arguments)] // Emission state is threaded explicitly for staging performance
345#[allow(clippy::too_many_lines)]
346#[allow(clippy::similar_names)] // Domain variable naming is intentional
347fn 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    // Determine JAR path from the stub's source_jar (set during scanning),
359    // falling back to the first provenance entry or a synthetic path.
360    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    // --- Class node ---
371    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    // --- Build ClasspathNodeMetadata (shared by class and all members) ---
394    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    // --- Methods ---
404    for method in &stub.methods {
405        let method_name_id = helper.intern(&method.name)?;
406        // Include the descriptor in the key to disambiguate overloaded methods.
407        // JVM methods are uniquely identified by (name, descriptor).
408        let method_fqn = format!("{}.{}{}", stub.fqn, method.name, method.descriptor);
409        #[allow(clippy::similar_names)] // Domain terminology: source/target node pairs
410        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        // Class → Method: Defines
431        staging.add_edge(class_node_id, method_node_id, EdgeKind::Defines, file_id);
432    }
433
434    // --- Fields ---
435    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        // Class → Field: Defines
459        staging.add_edge(class_node_id, field_node_id, EdgeKind::Defines, file_id);
460    }
461
462    // --- Enum constants ---
463    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        // Class → EnumConstant: Defines
485        staging.add_edge(class_node_id, const_node_id, EdgeKind::Defines, file_id);
486    }
487
488    // --- Type parameters ---
489    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            // Class → TypeParameter: TypeArgument
511            staging.add_edge(class_node_id, tp_node_id, EdgeKind::TypeArgument, file_id);
512        }
513    }
514
515    // --- Lambda targets ---
516    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        // Class → LambdaTarget: Contains
538        staging.add_edge(class_node_id, lambda_node_id, EdgeKind::Contains, file_id);
539    }
540
541    // --- Inner classes (Contains edge) ---
542    for inner in &stub.inner_classes {
543        // Only emit Contains edge if the inner class belongs to this outer class
544        // and has been separately emitted (will be linked post-emission).
545        if inner.outer_fqn.as_deref() == Some(&stub.fqn) {
546            // The inner class itself will be emitted as its own ClassStub.
547            // We record the relationship for edge creation in create_classpath_edges.
548            // Store the inner FQN in fqn_to_node so we can link later.
549            // Actual Contains edge is deferred to create_classpath_edges where
550            // both nodes are guaranteed to exist.
551        }
552    }
553
554    // --- Module info ---
555    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        // Class → Module: Contains
576        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
602// `find_jar_path_for_stub` has been replaced by `stub.source_jar` inline
603// in `emit_class_stub`. The JAR path is now tracked per-stub during scanning,
604// eliminating the incorrect "first provenance entry" heuristic.
605
606/// Look up provenance for a JAR path.
607fn 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// ---------------------------------------------------------------------------
615// Cross-reference edge creation (U15b)
616// ---------------------------------------------------------------------------
617
618/// Create inheritance, generic, annotation, module, and inner-class edges
619/// for classpath nodes.
620///
621/// Only creates edges where both source and target nodes exist in the graph.
622/// Missing targets are silently skipped (they may be from JARs not on the
623/// classpath).
624#[allow(clippy::too_many_lines)]
625#[allow(clippy::implicit_hasher)] // Standard HashMap intentional
626pub fn create_classpath_edges(
627    #[allow(clippy::implicit_hasher)] // Standard HashMap is intentional
628    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        // --- Inheritance (Inherits) ---
648        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        // --- Interface implementation (Implements) ---
666        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        // --- Generic bounds (GenericBound) ---
682        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                // Class bound
692                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                // Interface bounds
702                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        // --- Annotations (AnnotatedWith) ---
720        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        // Method-level annotations
735        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        // Field-level annotations
758        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        // --- Inner classes (Contains) ---
781        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        // --- Module edges ---
792        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            // ModuleRequires
801            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            // ModuleExports - edge from module to classes in exported package
812            for exp in &module.exports {
813                // Look up classes in the exported package
814                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            // ModuleOpens - edge from module to classes in opened package
831            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            // ModuleProvides - edge from module to service implementation classes
849            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
942/// Extract the FQN from a `TypeSignature::Class` variant.
943fn 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// ---------------------------------------------------------------------------
951// Tests
952// ---------------------------------------------------------------------------
953
954#[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    // -----------------------------------------------------------------------
967    // Test helpers
968    // -----------------------------------------------------------------------
969
970    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    /// Run emission and return the result plus the staging graph for inspection.
1044    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    // -----------------------------------------------------------------------
1086    // Test 1: Simple class emits correct nodes
1087    // -----------------------------------------------------------------------
1088
1089    #[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        // Class node
1099        assert!(
1100            result.fqn_to_node.contains_key("com.example.Foo"),
1101            "class node should be emitted"
1102        );
1103
1104        // Method nodes (key includes descriptor for overload disambiguation)
1105        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        // Field node
1115        assert!(
1116            result.fqn_to_node.contains_key("com.example.Foo.count"),
1117            "field 'count' should be emitted"
1118        );
1119
1120        // Total: 1 class + 2 methods + 1 field = 4
1121        assert_eq!(result.fqn_to_node.len(), 4);
1122    }
1123
1124    // -----------------------------------------------------------------------
1125    // Test 2: Inheritance edge
1126    // -----------------------------------------------------------------------
1127
1128    #[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        // Verify the AbstractList node exists
1143        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        // Edge verification happens through staging stats
1151        let stats = staging.stats();
1152        // Structural edges (Defines for methods/fields) + Inherits edge
1153        assert!(
1154            stats.edges_staged > 0,
1155            "should have at least one edge staged"
1156        );
1157    }
1158
1159    // -----------------------------------------------------------------------
1160    // Test 3: Interface implementation edge
1161    // -----------------------------------------------------------------------
1162
1163    #[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        // All three should exist
1191        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    // -----------------------------------------------------------------------
1326    // Test 6: ClasspathNodeMetadata attached correctly
1327    // -----------------------------------------------------------------------
1328
1329    #[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    // -----------------------------------------------------------------------
1370    // Test 7: Zero spans on all nodes
1371    // -----------------------------------------------------------------------
1372
1373    #[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        // All emitted nodes should have zero spans since they come from bytecode.
1382        // The NodeEntry::new constructor sets all span fields to 0 by default,
1383        // and we don't override them.
1384        assert!(
1385            !result.fqn_to_node.is_empty(),
1386            "should have emitted at least one node"
1387        );
1388
1389        // Verify through staging stats that nodes were emitted
1390        let stats = staging.stats();
1391        assert!(
1392            stats.nodes_staged >= 2,
1393            "should have at least 2 nodes (class + method)"
1394        );
1395    }
1396
1397    // -----------------------------------------------------------------------
1398    // Test 8: Annotation edges
1399    // -----------------------------------------------------------------------
1400
1401    #[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        // Also emit the annotation type so the edge can be created
1411        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        // Both nodes should exist
1425        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        // Edge verification through staging stats
1433        let stats = staging.stats();
1434        assert!(
1435            stats.edges_staged > 0,
1436            "should have annotation edges staged"
1437        );
1438    }
1439
1440    // -----------------------------------------------------------------------
1441    // Test 9: Module edges
1442    // -----------------------------------------------------------------------
1443
1444    #[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        // Module node should exist
1481        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    // -----------------------------------------------------------------------
1491    // Test 10: Enum constants
1492    // -----------------------------------------------------------------------
1493
1494    #[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    // -----------------------------------------------------------------------
1525    // Test 11: Type parameters emitted
1526    // -----------------------------------------------------------------------
1527
1528    #[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    // -----------------------------------------------------------------------
1559    // Test 12: Generic bound edges
1560    // -----------------------------------------------------------------------
1561
1562    #[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        // Type parameter and bound target should exist
1591        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    // -----------------------------------------------------------------------
1596    // Test 13: Lambda targets emitted
1597    // -----------------------------------------------------------------------
1598
1599    #[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    // -----------------------------------------------------------------------
1620    // Test 14: Inner class Contains edge
1621    // -----------------------------------------------------------------------
1622
1623    #[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    // -----------------------------------------------------------------------
1650    // Test 15: Empty index produces empty result
1651    // -----------------------------------------------------------------------
1652
1653    #[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    // -----------------------------------------------------------------------
1663    // Test 16: Synthetic file path convention
1664    // -----------------------------------------------------------------------
1665
1666    #[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    // -----------------------------------------------------------------------
1689    // Test 17: Interface kind maps correctly
1690    // -----------------------------------------------------------------------
1691
1692    #[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    // -----------------------------------------------------------------------
1704    // Test 18: Method-level annotation edge
1705    // -----------------------------------------------------------------------
1706
1707    #[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    // -----------------------------------------------------------------------
1748    // Test 19: No provenance still emits nodes
1749    // -----------------------------------------------------------------------
1750
1751    #[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    // -----------------------------------------------------------------------
1760    // Test 20: Visibility mapping
1761    // -----------------------------------------------------------------------
1762
1763    #[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}