Skip to main content

sqry_core/graph/unified/build/
helper.rs

1//! Helper utilities for `GraphBuilder` implementations.
2//!
3//! This module provides high-level abstractions that make it easier to
4//! implement `GraphBuilder::build_graph()` using the `StagingGraph` API.
5//!
6//! # Overview
7//!
8//! The [`GraphBuildHelper`] wraps a `&mut StagingGraph` and provides:
9//! - Local string interning with `StringId` tracking
10//! - Qualified name to `NodeId` mapping
11//! - High-level node creation methods
12//! - High-level edge creation methods
13#![allow(clippy::similar_names)] // Domain terminology uses caller/callee and importer/imported pairs.
14//!
15//! # Usage
16//!
17//! ```ignore
18//! fn build_graph(
19//!     &self,
20//!     tree: &Tree,
21//!     content: &[u8],
22//!     file: &Path,
23//!     staging: &mut StagingGraph,
24//! ) -> GraphResult<()> {
25//!     let mut helper = GraphBuildHelper::new(staging, file, Language::Rust);
26//!
27//!     // Create function nodes
28//!     let main_id = helper.add_function("main", None, false, false)?;
29//!     let helper_id = helper.add_function("helper", None, false, false)?;
30//!
31//!     // Create call edge
32//!     helper.add_call_edge(main_id, helper_id);
33//!
34//!     Ok(())
35//! }
36//! ```
37//!
38//! This helper provides a high-level API that mirrors the patterns plugins use
39//! with `StagingGraph`, reducing boilerplate in `GraphBuilder` implementations.
40
41use std::collections::HashMap;
42use std::path::Path;
43
44use super::super::edge::kind::{LifetimeConstraintKind, MacroExpansionKind, TypeOfContext};
45use super::super::resolution::canonicalize_graph_qualified_name;
46use super::staging::StagingGraph;
47use crate::graph::node::{Language, Span};
48use crate::graph::unified::edge::{EdgeKind, ExportKind, FfiConvention, HttpMethod, TableWriteOp};
49use crate::graph::unified::file::FileId;
50use crate::graph::unified::node::{NodeId, NodeKind};
51use crate::graph::unified::storage::NodeEntry;
52use crate::graph::unified::string::StringId;
53
54/// Helper for building graphs in `GraphBuilder` implementations.
55///
56/// Provides high-level abstractions over `StagingGraph` that handle:
57/// - String interning with local ID tracking
58/// - Qualified name deduplication
59/// - Node and edge creation with proper types
60#[derive(Debug)]
61pub struct GraphBuildHelper<'a> {
62    /// The underlying staging graph.
63    staging: &'a mut StagingGraph,
64    /// Language for this file.
65    language: Language,
66    /// File ID (pre-allocated).
67    file_id: FileId,
68    /// File path for error messages.
69    file_path: String,
70    /// Local string interning: string value -> local `StringId`.
71    string_cache: HashMap<String, StringId>,
72    /// Next local string ID to allocate.
73    next_string_id: u32,
74    /// Qualified name -> `NodeId` mapping for deduplication.
75    ///
76    /// Shared by both canonical nodes (via `add_node_internal`, which stores
77    /// under the **canonicalized** qualified name) and verbatim nodes (via
78    /// `add_node_verbatim`, which stores under the **raw** name).  Collisions
79    /// are avoided because canonical names never contain native delimiters
80    /// (e.g. `.`, `#`) while verbatim names preserve them (e.g. `styles.css`).
81    node_cache: HashMap<(String, NodeKind), NodeId>,
82}
83
84impl<'a> GraphBuildHelper<'a> {
85    /// Create a new helper for the given staging graph and file.
86    ///
87    /// The `file_id` should be pre-allocated by the caller (typically 0 for
88    /// per-file staging buffers).
89    pub fn new(staging: &'a mut StagingGraph, file: &Path, language: Language) -> Self {
90        Self {
91            staging,
92            language,
93            file_id: FileId::new(0), // Per-file staging uses local file ID 0
94            file_path: file.display().to_string(),
95            string_cache: HashMap::new(),
96            next_string_id: 0,
97            node_cache: HashMap::new(),
98        }
99    }
100
101    /// Create a helper with a specific file ID.
102    pub fn with_file_id(
103        staging: &'a mut StagingGraph,
104        file: &Path,
105        language: Language,
106        file_id: FileId,
107    ) -> Self {
108        Self {
109            staging,
110            language,
111            file_id,
112            file_path: file.display().to_string(),
113            string_cache: HashMap::new(),
114            next_string_id: 0,
115            node_cache: HashMap::new(),
116        }
117    }
118
119    /// Get the language for this helper.
120    #[must_use]
121    pub fn language(&self) -> Language {
122        self.language
123    }
124
125    /// Get the file ID for this helper.
126    #[must_use]
127    pub fn file_id(&self) -> FileId {
128        self.file_id
129    }
130
131    /// Get the file path.
132    #[must_use]
133    pub fn file_path(&self) -> &str {
134        &self.file_path
135    }
136
137    /// Intern a string and get a local `StringId`.
138    ///
139    /// Strings are deduplicated: calling with the same value returns the same ID.
140    /// The local `StringId` is passed to the staging graph so that during
141    /// `commit_strings()`, a remap table from local to global IDs can be built.
142    pub fn intern(&mut self, s: &str) -> StringId {
143        if let Some(&id) = self.string_cache.get(s) {
144            return id;
145        }
146
147        let id = StringId::new_local(self.next_string_id);
148        self.next_string_id += 1;
149        self.string_cache.insert(s.to_string(), id);
150        // Pass the local_id to staging so it can build the remap table during commit
151        self.staging.intern_string(id, s.to_string());
152        id
153    }
154
155    /// Check if a node with the given qualified name already exists.
156    #[must_use]
157    pub fn has_node(&self, qualified_name: &str) -> bool {
158        self.node_cache
159            .keys()
160            .any(|(name, _)| name == qualified_name)
161    }
162
163    /// Get an existing node by qualified name.
164    #[must_use]
165    pub fn get_node(&self, qualified_name: &str) -> Option<NodeId> {
166        self.node_cache
167            .iter()
168            .find_map(|((name, _), id)| (name == qualified_name).then_some(*id))
169    }
170
171    /// Check if a node with the given qualified name and kind already exists.
172    #[must_use]
173    pub fn has_node_with_kind(&self, qualified_name: &str, kind: NodeKind) -> bool {
174        self.node_cache
175            .contains_key(&(qualified_name.to_string(), kind))
176    }
177
178    /// Get an existing node by qualified name and kind.
179    #[must_use]
180    pub fn get_node_with_kind(&self, qualified_name: &str, kind: NodeKind) -> Option<NodeId> {
181        self.node_cache
182            .get(&(qualified_name.to_string(), kind))
183            .copied()
184    }
185
186    /// Add a function node with the given qualified name.
187    ///
188    /// Returns the `NodeId` (creating the node if it doesn't exist).
189    pub fn add_function(
190        &mut self,
191        qualified_name: &str,
192        span: Option<Span>,
193        is_async: bool,
194        is_unsafe: bool,
195    ) -> NodeId {
196        self.add_node_internal(
197            qualified_name,
198            span,
199            NodeKind::Function,
200            &[("async", is_async), ("unsafe", is_unsafe)],
201            None,
202            None,
203        )
204    }
205
206    /// Add a function node with visibility.
207    ///
208    /// Returns the `NodeId` (creating the node if it doesn't exist).
209    pub fn add_function_with_visibility(
210        &mut self,
211        qualified_name: &str,
212        span: Option<Span>,
213        is_async: bool,
214        is_unsafe: bool,
215        visibility: Option<&str>,
216    ) -> NodeId {
217        self.add_node_internal(
218            qualified_name,
219            span,
220            NodeKind::Function,
221            &[("async", is_async), ("unsafe", is_unsafe)],
222            visibility,
223            None,
224        )
225    }
226
227    /// Add a function node with signature (return type).
228    ///
229    /// The signature is used for `returns:` queries.
230    /// Returns the `NodeId` (creating the node if it doesn't exist).
231    pub fn add_function_with_signature(
232        &mut self,
233        qualified_name: &str,
234        span: Option<Span>,
235        is_async: bool,
236        is_unsafe: bool,
237        visibility: Option<&str>,
238        signature: Option<&str>,
239    ) -> NodeId {
240        self.add_node_internal(
241            qualified_name,
242            span,
243            NodeKind::Function,
244            &[("async", is_async), ("unsafe", is_unsafe)],
245            visibility,
246            signature,
247        )
248    }
249
250    /// Add a method node with the given qualified name.
251    pub fn add_method(
252        &mut self,
253        qualified_name: &str,
254        span: Option<Span>,
255        is_async: bool,
256        is_static: bool,
257    ) -> NodeId {
258        self.add_node_internal(
259            qualified_name,
260            span,
261            NodeKind::Method,
262            &[("async", is_async), ("static", is_static)],
263            None,
264            None,
265        )
266    }
267
268    /// Add a method node with visibility.
269    pub fn add_method_with_visibility(
270        &mut self,
271        qualified_name: &str,
272        span: Option<Span>,
273        is_async: bool,
274        is_static: bool,
275        visibility: Option<&str>,
276    ) -> NodeId {
277        self.add_node_internal(
278            qualified_name,
279            span,
280            NodeKind::Method,
281            &[("async", is_async), ("static", is_static)],
282            visibility,
283            None,
284        )
285    }
286
287    /// Add a method node with signature (return type).
288    ///
289    /// The signature is used for `returns:` queries.
290    /// Returns the `NodeId` (creating the node if it doesn't exist).
291    pub fn add_method_with_signature(
292        &mut self,
293        qualified_name: &str,
294        span: Option<Span>,
295        is_async: bool,
296        is_static: bool,
297        visibility: Option<&str>,
298        signature: Option<&str>,
299    ) -> NodeId {
300        self.add_node_internal(
301            qualified_name,
302            span,
303            NodeKind::Method,
304            &[("async", is_async), ("static", is_static)],
305            visibility,
306            signature,
307        )
308    }
309
310    /// Add a class node.
311    pub fn add_class(&mut self, qualified_name: &str, span: Option<Span>) -> NodeId {
312        self.add_node_internal(qualified_name, span, NodeKind::Class, &[], None, None)
313    }
314
315    /// Add a class node with visibility.
316    pub fn add_class_with_visibility(
317        &mut self,
318        qualified_name: &str,
319        span: Option<Span>,
320        visibility: Option<&str>,
321    ) -> NodeId {
322        self.add_node_internal(qualified_name, span, NodeKind::Class, &[], visibility, None)
323    }
324
325    /// Add a struct node.
326    pub fn add_struct(&mut self, qualified_name: &str, span: Option<Span>) -> NodeId {
327        self.add_node_internal(qualified_name, span, NodeKind::Struct, &[], None, None)
328    }
329
330    /// Add a struct node with visibility.
331    pub fn add_struct_with_visibility(
332        &mut self,
333        qualified_name: &str,
334        span: Option<Span>,
335        visibility: Option<&str>,
336    ) -> NodeId {
337        self.add_node_internal(
338            qualified_name,
339            span,
340            NodeKind::Struct,
341            &[],
342            visibility,
343            None,
344        )
345    }
346
347    /// Add a module node.
348    pub fn add_module(&mut self, qualified_name: &str, span: Option<Span>) -> NodeId {
349        self.add_node_internal(qualified_name, span, NodeKind::Module, &[], None, None)
350    }
351
352    /// Add a resource node.
353    pub fn add_resource(&mut self, qualified_name: &str, span: Option<Span>) -> NodeId {
354        self.add_node_internal(qualified_name, span, NodeKind::Resource, &[], None, None)
355    }
356
357    /// Add an endpoint node for HTTP route handlers.
358    ///
359    /// The qualified name should follow the convention `route::{METHOD}::{path}`,
360    /// for example `route::GET::/api/users` or `route::POST::/api/items`.
361    ///
362    /// Endpoint nodes are used by Pass 5 (cross-language linking) to match
363    /// HTTP requests from client code to server-side route handlers.
364    pub fn add_endpoint(&mut self, qualified_name: &str, span: Option<Span>) -> NodeId {
365        self.add_node_internal(qualified_name, span, NodeKind::Endpoint, &[], None, None)
366    }
367
368    /// Add an import node.
369    pub fn add_import(&mut self, qualified_name: &str, span: Option<Span>) -> NodeId {
370        self.add_node_internal(qualified_name, span, NodeKind::Import, &[], None, None)
371    }
372
373    /// Add an import node while preserving the original path-like identifier.
374    ///
375    /// Use this for resource imports such as `styles.css`, `app.js`, or
376    /// similar asset filenames where `.` is part of the path rather than a
377    /// language-native qualified-name separator.
378    pub fn add_verbatim_import(&mut self, name: &str, span: Option<Span>) -> NodeId {
379        self.add_node_verbatim(name, span, NodeKind::Import, &[], None, None)
380    }
381
382    /// Add a variable node.
383    pub fn add_variable(&mut self, qualified_name: &str, span: Option<Span>) -> NodeId {
384        self.add_node_internal(qualified_name, span, NodeKind::Variable, &[], None, None)
385    }
386
387    /// Add a variable node while preserving the original identifier exactly.
388    ///
389    /// Use this for static asset references where the literal path is the
390    /// graph identity.
391    pub fn add_verbatim_variable(&mut self, name: &str, span: Option<Span>) -> NodeId {
392        self.add_node_verbatim(name, span, NodeKind::Variable, &[], None, None)
393    }
394
395    /// Add a constant node.
396    pub fn add_constant(&mut self, qualified_name: &str, span: Option<Span>) -> NodeId {
397        self.add_node_internal(qualified_name, span, NodeKind::Constant, &[], None, None)
398    }
399
400    /// Add a constant node with visibility.
401    pub fn add_constant_with_visibility(
402        &mut self,
403        qualified_name: &str,
404        span: Option<Span>,
405        visibility: Option<&str>,
406    ) -> NodeId {
407        self.add_node_internal(
408            qualified_name,
409            span,
410            NodeKind::Constant,
411            &[],
412            visibility,
413            None,
414        )
415    }
416
417    /// Add a constant node with static and visibility attributes.
418    pub fn add_constant_with_static_and_visibility(
419        &mut self,
420        qualified_name: &str,
421        span: Option<Span>,
422        is_static: bool,
423        visibility: Option<&str>,
424    ) -> NodeId {
425        let attrs: &[(&str, bool)] = if is_static { &[("static", true)] } else { &[] };
426        self.add_node_internal(
427            qualified_name,
428            span,
429            NodeKind::Constant,
430            attrs,
431            visibility,
432            None,
433        )
434    }
435
436    /// Add a property node with static and visibility attributes.
437    pub fn add_property_with_static_and_visibility(
438        &mut self,
439        qualified_name: &str,
440        span: Option<Span>,
441        is_static: bool,
442        visibility: Option<&str>,
443    ) -> NodeId {
444        let attrs: &[(&str, bool)] = if is_static { &[("static", true)] } else { &[] };
445        self.add_node_internal(
446            qualified_name,
447            span,
448            NodeKind::Property,
449            attrs,
450            visibility,
451            None,
452        )
453    }
454
455    /// Add an enum node.
456    pub fn add_enum(&mut self, qualified_name: &str, span: Option<Span>) -> NodeId {
457        self.add_node_internal(qualified_name, span, NodeKind::Enum, &[], None, None)
458    }
459
460    /// Add an enum node with visibility.
461    pub fn add_enum_with_visibility(
462        &mut self,
463        qualified_name: &str,
464        span: Option<Span>,
465        visibility: Option<&str>,
466    ) -> NodeId {
467        self.add_node_internal(qualified_name, span, NodeKind::Enum, &[], visibility, None)
468    }
469
470    /// Add an interface/trait node.
471    pub fn add_interface(&mut self, qualified_name: &str, span: Option<Span>) -> NodeId {
472        self.add_node_internal(qualified_name, span, NodeKind::Interface, &[], None, None)
473    }
474
475    /// Add an interface/trait node with visibility.
476    pub fn add_interface_with_visibility(
477        &mut self,
478        qualified_name: &str,
479        span: Option<Span>,
480        visibility: Option<&str>,
481    ) -> NodeId {
482        self.add_node_internal(
483            qualified_name,
484            span,
485            NodeKind::Interface,
486            &[],
487            visibility,
488            None,
489        )
490    }
491
492    /// Add a type alias node.
493    pub fn add_type(&mut self, qualified_name: &str, span: Option<Span>) -> NodeId {
494        self.add_node_internal(qualified_name, span, NodeKind::Type, &[], None, None)
495    }
496
497    /// Add a type alias node with visibility.
498    pub fn add_type_with_visibility(
499        &mut self,
500        qualified_name: &str,
501        span: Option<Span>,
502        visibility: Option<&str>,
503    ) -> NodeId {
504        self.add_node_internal(qualified_name, span, NodeKind::Type, &[], visibility, None)
505    }
506
507    /// Add a lifetime node.
508    pub fn add_lifetime(&mut self, qualified_name: &str, span: Option<Span>) -> NodeId {
509        self.add_node_internal(qualified_name, span, NodeKind::Lifetime, &[], None, None)
510    }
511
512    /// Add a lifetime constraint edge.
513    pub fn add_lifetime_constraint_edge(
514        &mut self,
515        source: NodeId,
516        target: NodeId,
517        constraint_kind: LifetimeConstraintKind,
518    ) {
519        self.staging.add_edge(
520            source,
521            target,
522            EdgeKind::LifetimeConstraint { constraint_kind },
523            self.file_id,
524        );
525    }
526
527    /// Add a trait method binding edge.
528    ///
529    /// This edge represents the resolution of a trait method call to a concrete
530    /// implementation.
531    pub fn add_trait_method_binding_edge(
532        &mut self,
533        caller: NodeId,
534        callee: NodeId,
535        trait_name: &str,
536        impl_type: &str,
537        is_ambiguous: bool,
538    ) {
539        let trait_name_id = self.intern(trait_name);
540        let impl_type_id = self.intern(impl_type);
541        self.staging.add_edge(
542            caller,
543            callee,
544            EdgeKind::TraitMethodBinding {
545                trait_name: trait_name_id,
546                impl_type: impl_type_id,
547                is_ambiguous,
548            },
549            self.file_id,
550        );
551    }
552
553    /// Add a macro expansion edge.
554    ///
555    /// Represents the expansion of a macro invocation to its generated code.
556    /// Only available when macro expansion is enabled.
557    ///
558    /// # Arguments
559    ///
560    /// * `invocation` - The macro invocation site node (e.g., derive attribute or macro call)
561    /// * `expansion` - The macro definition or generated code node
562    /// * `expansion_kind` - The kind of macro expansion (Derive, Attribute, Declarative, Function)
563    /// * `is_verified` - Whether the expansion has been verified (requires `cargo expand`)
564    ///
565    /// # Example
566    ///
567    /// ```ignore
568    /// // #[derive(Debug)] on a struct
569    /// let struct_id = helper.add_struct("MyStruct", Some(span));
570    /// let derive_macro_id = helper.add_node("MyStruct::derive_Debug", None, NodeKind::Macro);
571    /// helper.add_macro_expansion_edge(
572    ///     struct_id,
573    ///     derive_macro_id,
574    ///     MacroExpansionKind::Derive,
575    ///     false,
576    /// );
577    /// ```
578    pub fn add_macro_expansion_edge(
579        &mut self,
580        invocation: NodeId,
581        expansion: NodeId,
582        expansion_kind: MacroExpansionKind,
583        is_verified: bool,
584    ) {
585        self.staging.add_edge(
586            invocation,
587            expansion,
588            EdgeKind::MacroExpansion {
589                expansion_kind,
590                is_verified,
591            },
592            self.file_id,
593        );
594    }
595
596    /// Add a generic node with custom kind.
597    pub fn add_node(&mut self, qualified_name: &str, span: Option<Span>, kind: NodeKind) -> NodeId {
598        self.add_node_internal(qualified_name, span, kind, &[], None, None)
599    }
600
601    /// Add a generic node with visibility.
602    pub fn add_node_with_visibility(
603        &mut self,
604        qualified_name: &str,
605        span: Option<Span>,
606        kind: NodeKind,
607        visibility: Option<&str>,
608    ) -> NodeId {
609        self.add_node_internal(qualified_name, span, kind, &[], visibility, None)
610    }
611
612    /// Internal helper for adding nodes.
613    ///
614    /// Applies attributes to the node entry:
615    /// - `"async"` → `NodeEntry::with_async(true/false)`
616    /// - `"static"` → `NodeEntry::with_static(true/false)`
617    /// - `"unsafe"` → `NodeEntry::with_unsafe(true/false)`
618    ///
619    /// When `signature` is `Some`, the signature field is set on the node for
620    /// `returns:` queries.
621    fn add_node_internal(
622        &mut self,
623        qualified_name: &str,
624        span: Option<Span>,
625        kind: NodeKind,
626        attributes: &[(&str, bool)],
627        visibility: Option<&str>,
628        signature: Option<&str>,
629    ) -> NodeId {
630        let canonical_qualified_name =
631            canonicalize_graph_qualified_name(self.language, qualified_name);
632        let semantic_name = semantic_name_for_node_input(qualified_name, &canonical_qualified_name);
633        let mut is_async = false;
634        let mut is_static = false;
635        let mut is_unsafe = false;
636        for &(key, value) in attributes {
637            match key {
638                "async" => is_async |= value,
639                "static" => is_static |= value,
640                "unsafe" => is_unsafe |= value,
641                _ => {}
642            }
643        }
644
645        // Check cache first
646        if let Some(&id) = self
647            .node_cache
648            .get(&(canonical_qualified_name.clone(), kind))
649        {
650            let visibility_id = visibility.map(|vis| self.intern(vis));
651            let signature_id = signature.map(|sig| self.intern(sig));
652            self.staging.update_node_entry(
653                id,
654                span,
655                is_async,
656                is_static,
657                is_unsafe,
658                visibility_id,
659                signature_id,
660            );
661            return id;
662        }
663
664        let name_id = self.intern(&semantic_name);
665
666        // Create node entry
667        let mut entry = NodeEntry::new(kind, name_id, self.file_id);
668        if semantic_name != canonical_qualified_name {
669            let qualified_name_id = self.intern(&canonical_qualified_name);
670            entry = entry.with_qualified_name(qualified_name_id);
671        }
672
673        // Set span if provided
674        if let Some(s) = span {
675            let start_line = u32::try_from(s.start.line.saturating_add(1)).unwrap_or(u32::MAX);
676            let start_column = u32::try_from(s.start.column).unwrap_or(u32::MAX);
677            let end_line = u32::try_from(s.end.line.saturating_add(1)).unwrap_or(u32::MAX);
678            let end_column = u32::try_from(s.end.column).unwrap_or(u32::MAX);
679            entry = entry.with_location(start_line, start_column, end_line, end_column);
680        }
681
682        // Apply attributes to node entry
683        if is_async {
684            entry = entry.with_async(true);
685        }
686        if is_static {
687            entry = entry.with_static(true);
688        }
689        if is_unsafe {
690            entry = entry.with_unsafe(true);
691        }
692
693        // Apply visibility if provided
694        if let Some(vis) = visibility {
695            let vis_id = self.intern(vis);
696            entry = entry.with_visibility(vis_id);
697        }
698
699        // Apply signature (return type) if provided
700        if let Some(sig) = signature {
701            let sig_id = self.intern(sig);
702            entry = entry.with_signature(sig_id);
703        }
704
705        // Stage the node
706        let node_id = self.staging.add_node(entry);
707
708        // Cache for deduplication
709        self.node_cache
710            .insert((canonical_qualified_name, kind), node_id);
711
712        node_id
713    }
714
715    fn add_node_verbatim(
716        &mut self,
717        name: &str,
718        span: Option<Span>,
719        kind: NodeKind,
720        attributes: &[(&str, bool)],
721        visibility: Option<&str>,
722        signature: Option<&str>,
723    ) -> NodeId {
724        let mut is_async = false;
725        let mut is_static = false;
726        let mut is_unsafe = false;
727        for &(key, value) in attributes {
728            match key {
729                "async" => is_async |= value,
730                "static" => is_static |= value,
731                "unsafe" => is_unsafe |= value,
732                _ => {}
733            }
734        }
735
736        if let Some(&id) = self.node_cache.get(&(name.to_string(), kind)) {
737            let visibility_id = visibility.map(|vis| self.intern(vis));
738            let signature_id = signature.map(|sig| self.intern(sig));
739            self.staging.update_node_entry(
740                id,
741                span,
742                is_async,
743                is_static,
744                is_unsafe,
745                visibility_id,
746                signature_id,
747            );
748            return id;
749        }
750
751        let name_id = self.intern(name);
752        let mut entry = NodeEntry::new(kind, name_id, self.file_id);
753
754        if let Some(s) = span {
755            let start_line = u32::try_from(s.start.line.saturating_add(1)).unwrap_or(u32::MAX);
756            let start_column = u32::try_from(s.start.column).unwrap_or(u32::MAX);
757            let end_line = u32::try_from(s.end.line.saturating_add(1)).unwrap_or(u32::MAX);
758            let end_column = u32::try_from(s.end.column).unwrap_or(u32::MAX);
759            entry = entry.with_location(start_line, start_column, end_line, end_column);
760        }
761
762        if is_async {
763            entry = entry.with_async(true);
764        }
765        if is_static {
766            entry = entry.with_static(true);
767        }
768        if is_unsafe {
769            entry = entry.with_unsafe(true);
770        }
771
772        if let Some(vis) = visibility {
773            let vis_id = self.intern(vis);
774            entry = entry.with_visibility(vis_id);
775        }
776        if let Some(sig) = signature {
777            let sig_id = self.intern(sig);
778            entry = entry.with_signature(sig_id);
779        }
780
781        let node_id = self.staging.add_node(entry);
782        self.node_cache.insert((name.to_string(), kind), node_id);
783        node_id
784    }
785
786    /// Add a call edge from caller to callee.
787    pub fn add_call_edge(&mut self, caller: NodeId, callee: NodeId) {
788        self.add_call_edge_with_span(caller, callee, Vec::new());
789    }
790
791    /// Add a call edge from caller to callee with source span information.
792    ///
793    /// The span should point to the call site location in source code.
794    ///
795    /// # Note
796    ///
797    /// This method uses default metadata (`argument_count: 255` sentinel for unknown, `is_async: false`).
798    /// Use [`add_call_edge_full`](Self::add_call_edge_full) when you need to specify
799    /// argument count or async status explicitly.
800    pub fn add_call_edge_with_span(
801        &mut self,
802        caller: NodeId,
803        callee: NodeId,
804        spans: Vec<crate::graph::node::Span>,
805    ) {
806        self.staging.add_edge_with_spans(
807            caller,
808            callee,
809            EdgeKind::Calls {
810                argument_count: 255,
811                is_async: false,
812            },
813            self.file_id,
814            spans,
815        );
816    }
817
818    /// Add a call edge with full metadata.
819    ///
820    /// Use this method when you know the argument count or when the call is async.
821    /// For calls where metadata is unknown, use [`add_call_edge`](Self::add_call_edge)
822    /// which uses default values (`argument_count: 255` sentinel, `is_async: false`).
823    ///
824    /// # Arguments
825    ///
826    /// * `caller` - The node making the call
827    /// * `callee` - The node being called
828    /// * `argument_count` - Number of arguments in the call (0-254, use 255 for unknown)
829    /// * `is_async` - Whether this is an async/await call
830    ///
831    /// # Canonical Usage
832    ///
833    /// | Scenario | Method |
834    /// |----------|--------|
835    /// | Argument count known, sync call | `add_call_edge_full(caller, callee, arg_count, false)` |
836    /// | Argument count known, async call | `add_call_edge_full(caller, callee, arg_count, true)` |
837    /// | Argument count unknown, sync call | `add_call_edge(caller, callee)` or `add_call_edge_full(caller, callee, 255, false)` |
838    ///
839    /// # Example
840    ///
841    /// ```ignore
842    /// // Function call with 3 arguments
843    /// helper.add_call_edge_full(main_id, helper_id, 3, false);
844    ///
845    /// // Async call with 1 argument
846    /// helper.add_call_edge_full(main_id, async_fn_id, 1, true);
847    /// ```
848    pub fn add_call_edge_full(
849        &mut self,
850        caller: NodeId,
851        callee: NodeId,
852        argument_count: u8,
853        is_async: bool,
854    ) {
855        self.staging.add_edge(
856            caller,
857            callee,
858            EdgeKind::Calls {
859                argument_count,
860                is_async,
861            },
862            self.file_id,
863        );
864    }
865
866    /// Add a call edge with full metadata and source span information.
867    ///
868    /// Combines the functionality of [`add_call_edge_full`](Self::add_call_edge_full)
869    /// and span tracking.
870    pub fn add_call_edge_full_with_span(
871        &mut self,
872        caller: NodeId,
873        callee: NodeId,
874        argument_count: u8,
875        is_async: bool,
876        spans: Vec<crate::graph::node::Span>,
877    ) {
878        self.staging.add_edge_with_spans(
879            caller,
880            callee,
881            EdgeKind::Calls {
882                argument_count,
883                is_async,
884            },
885            self.file_id,
886            spans,
887        );
888    }
889
890    /// Add a database table read edge (SQL).
891    pub fn add_table_read_edge_with_span(
892        &mut self,
893        reader: NodeId,
894        table: NodeId,
895        table_name: &str,
896        schema: Option<&str>,
897        spans: Vec<crate::graph::node::Span>,
898    ) {
899        let table_name_id = self.intern(table_name);
900        let schema_id = schema.map(|s| self.intern(s));
901        self.staging.add_edge_with_spans(
902            reader,
903            table,
904            EdgeKind::TableRead {
905                table_name: table_name_id,
906                schema: schema_id,
907            },
908            self.file_id,
909            spans,
910        );
911    }
912
913    /// Add a database table write edge (SQL).
914    pub fn add_table_write_edge_with_span(
915        &mut self,
916        writer: NodeId,
917        table: NodeId,
918        table_name: &str,
919        schema: Option<&str>,
920        operation: TableWriteOp,
921        spans: Vec<crate::graph::node::Span>,
922    ) {
923        let table_name_id = self.intern(table_name);
924        let schema_id = schema.map(|s| self.intern(s));
925        self.staging.add_edge_with_spans(
926            writer,
927            table,
928            EdgeKind::TableWrite {
929                table_name: table_name_id,
930                schema: schema_id,
931                operation,
932            },
933            self.file_id,
934            spans,
935        );
936    }
937
938    /// Add a database trigger relationship edge (SQL).
939    ///
940    /// Convention: `trigger -> table` with `EdgeKind::TriggeredBy`.
941    pub fn add_triggered_by_edge_with_span(
942        &mut self,
943        trigger: NodeId,
944        table: NodeId,
945        trigger_name: &str,
946        schema: Option<&str>,
947        spans: Vec<crate::graph::node::Span>,
948    ) {
949        let trigger_name_id = self.intern(trigger_name);
950        let schema_id = schema.map(|s| self.intern(s));
951        self.staging.add_edge_with_spans(
952            trigger,
953            table,
954            EdgeKind::TriggeredBy {
955                trigger_name: trigger_name_id,
956                schema: schema_id,
957            },
958            self.file_id,
959            spans,
960        );
961    }
962
963    /// Add an import edge from importer to imported module/symbol.
964    ///
965    /// This method uses default metadata (`alias: None`, `is_wildcard: false`).
966    /// Use [`add_import_edge_full`](Self::add_import_edge_full) when importing
967    /// with an alias or for wildcard imports.
968    pub fn add_import_edge(&mut self, importer: NodeId, imported: NodeId) {
969        self.staging.add_edge(
970            importer,
971            imported,
972            EdgeKind::Imports {
973                alias: None,
974                is_wildcard: false,
975            },
976            self.file_id,
977        );
978    }
979
980    /// Add an import edge with full metadata.
981    ///
982    /// Use this method when the import has an alias or is a wildcard import.
983    /// For simple imports without alias or wildcard, use [`add_import_edge`](Self::add_import_edge).
984    ///
985    /// # Arguments
986    ///
987    /// * `importer` - The node importing (e.g., module or file)
988    /// * `imported` - The node being imported
989    /// * `alias` - Optional alias string (e.g., for `import { foo as bar }`, alias is "bar")
990    /// * `is_wildcard` - Whether this is a wildcard import (e.g., `import *`)
991    ///
992    /// # Canonical Usage
993    ///
994    /// | Import Syntax | Method |
995    /// |---------------|--------|
996    /// | `import foo` | `add_import_edge(importer, imported)` |
997    /// | `import foo as bar` | `add_import_edge_full(importer, imported, Some("bar"), false)` |
998    /// | `import *` / `import *.*` | `add_import_edge_full(importer, imported, None, true)` |
999    /// | `import * as ns` | `add_import_edge_full(importer, imported, Some("ns"), true)` |
1000    ///
1001    /// # Example
1002    ///
1003    /// ```ignore
1004    /// // import { HashMap as Map } from "std::collections"
1005    /// let alias_id = helper.intern("Map");
1006    /// helper.add_import_edge_full(module_id, hashmap_id, Some("Map"), false);
1007    ///
1008    /// // import * from "lodash"
1009    /// helper.add_import_edge_full(module_id, lodash_id, None, true);
1010    /// ```
1011    pub fn add_import_edge_full(
1012        &mut self,
1013        importer: NodeId,
1014        imported: NodeId,
1015        alias: Option<&str>,
1016        is_wildcard: bool,
1017    ) {
1018        let alias_id = alias.map(|s| self.intern(s));
1019        self.staging.add_edge(
1020            importer,
1021            imported,
1022            EdgeKind::Imports {
1023                alias: alias_id,
1024                is_wildcard,
1025            },
1026            self.file_id,
1027        );
1028    }
1029
1030    /// Add an export edge from module to exported symbol.
1031    ///
1032    /// This method uses default metadata (`kind: ExportKind::Direct`, `alias: None`).
1033    /// Use [`add_export_edge_full`](Self::add_export_edge_full) for re-exports,
1034    /// default exports, namespace exports, or exports with aliases.
1035    pub fn add_export_edge(&mut self, module: NodeId, exported: NodeId) {
1036        self.staging.add_edge(
1037            module,
1038            exported,
1039            EdgeKind::Exports {
1040                kind: ExportKind::Direct,
1041                alias: None,
1042            },
1043            self.file_id,
1044        );
1045    }
1046
1047    /// Add an export edge with full metadata.
1048    ///
1049    /// Use this method for re-exports, default exports, namespace exports,
1050    /// or exports with aliases. For simple direct exports without alias,
1051    /// use [`add_export_edge`](Self::add_export_edge).
1052    ///
1053    /// # Arguments
1054    ///
1055    /// * `module` - The module/file node that contains the export
1056    /// * `exported` - The symbol being exported
1057    /// * `kind` - The kind of export:
1058    ///   - `ExportKind::Direct` - Direct export (`export { foo }`)
1059    ///   - `ExportKind::Reexport` - Re-export from another module (`export { foo } from "mod"`)
1060    ///   - `ExportKind::Default` - Default export (`export default foo`)
1061    ///   - `ExportKind::Namespace` - Namespace export (`export * as ns from "mod"`)
1062    /// * `alias` - Optional alias string (e.g., for `export { foo as bar }`, alias is "bar")
1063    ///
1064    /// # Canonical Usage
1065    ///
1066    /// | Export Syntax (JS/TS) | Method |
1067    /// |-----------------------|--------|
1068    /// | `export { name }` | `add_export_edge(module, name)` |
1069    /// | `export default foo` | `add_export_edge_full(module, foo, ExportKind::Default, None)` |
1070    /// | `export { foo as bar }` | `add_export_edge_full(module, foo, ExportKind::Direct, Some("bar"))` |
1071    /// | `export { foo } from "mod"` | `add_export_edge_full(module, foo, ExportKind::Reexport, None)` |
1072    /// | `export { foo as bar } from "mod"` | `add_export_edge_full(module, foo, ExportKind::Reexport, Some("bar"))` |
1073    /// | `export * from "mod"` | `add_export_edge_full(module, mod, ExportKind::Reexport, None)` |
1074    /// | `export * as ns from "mod"` | `add_export_edge_full(module, mod, ExportKind::Namespace, Some("ns"))` |
1075    ///
1076    /// # Example
1077    ///
1078    /// ```ignore
1079    /// // export default MyComponent;
1080    /// helper.add_export_edge_full(module_id, component_id, ExportKind::Default, None);
1081    ///
1082    /// // export { helper as utilHelper };
1083    /// helper.add_export_edge_full(module_id, helper_id, ExportKind::Direct, Some("utilHelper"));
1084    ///
1085    /// // export * as utils from "./utils";
1086    /// helper.add_export_edge_full(module_id, utils_id, ExportKind::Namespace, Some("utils"));
1087    /// ```
1088    pub fn add_export_edge_full(
1089        &mut self,
1090        module: NodeId,
1091        exported: NodeId,
1092        kind: ExportKind,
1093        alias: Option<&str>,
1094    ) {
1095        let alias_id = alias.map(|s| self.intern(s));
1096        self.staging.add_edge(
1097            module,
1098            exported,
1099            EdgeKind::Exports {
1100                kind,
1101                alias: alias_id,
1102            },
1103            self.file_id,
1104        );
1105    }
1106
1107    /// Add a reference edge (variable/field access).
1108    pub fn add_reference_edge(&mut self, from: NodeId, to: NodeId) {
1109        self.staging
1110            .add_edge(from, to, EdgeKind::References, self.file_id);
1111    }
1112
1113    /// Add a defines edge (module defines symbol).
1114    pub fn add_defines_edge(&mut self, parent: NodeId, child: NodeId) {
1115        self.staging
1116            .add_edge(parent, child, EdgeKind::Defines, self.file_id);
1117    }
1118
1119    /// Add a type-of edge (symbol has type).
1120    /// Add a `TypeOf` edge without context metadata (backward compatibility).
1121    ///
1122    /// For new code, prefer `add_typeof_edge_with_context` to provide semantic context.
1123    pub fn add_typeof_edge(&mut self, source: NodeId, target: NodeId) {
1124        self.add_typeof_edge_with_context(source, target, None, None, None);
1125    }
1126
1127    /// Add a `TypeOf` edge with optional context metadata.
1128    ///
1129    /// # Parameters
1130    /// - `source`: The node that has this type (e.g., variable, function, parameter)
1131    /// - `target`: The type node
1132    /// - `context`: Where this type reference appears (Parameter, Return, Field, Variable, etc.)
1133    /// - `index`: Position/index (for parameters, returns, fields)
1134    /// - `name`: Name (for parameters, returns, fields, variables)
1135    ///
1136    /// # Examples
1137    /// ```ignore
1138    /// // Function parameter: func foo(ctx context.Context)
1139    /// helper.add_typeof_edge_with_context(
1140    ///     func_id,
1141    ///     type_id,
1142    ///     Some(TypeOfContext::Parameter),
1143    ///     Some(0),
1144    ///     Some("ctx"),
1145    /// );
1146    ///
1147    /// // Function return: func bar() error
1148    /// helper.add_typeof_edge_with_context(
1149    ///     func_id,
1150    ///     error_type_id,
1151    ///     Some(TypeOfContext::Return),
1152    ///     Some(0),
1153    ///     None,
1154    /// );
1155    ///
1156    /// // Variable: var x int
1157    /// helper.add_typeof_edge_with_context(
1158    ///     var_id,
1159    ///     int_type_id,
1160    ///     Some(TypeOfContext::Variable),
1161    ///     None,
1162    ///     Some("x"),
1163    /// );
1164    /// ```
1165    pub fn add_typeof_edge_with_context(
1166        &mut self,
1167        source: NodeId,
1168        target: NodeId,
1169        context: Option<TypeOfContext>,
1170        index: Option<u16>,
1171        name: Option<&str>,
1172    ) {
1173        let name_id = name.map(|n| self.intern(n));
1174        self.staging.add_edge(
1175            source,
1176            target,
1177            EdgeKind::TypeOf {
1178                context,
1179                index,
1180                name: name_id,
1181            },
1182            self.file_id,
1183        );
1184    }
1185
1186    /// Add an implements edge (class implements interface).
1187    pub fn add_implements_edge(&mut self, implementor: NodeId, interface: NodeId) {
1188        self.staging
1189            .add_edge(implementor, interface, EdgeKind::Implements, self.file_id);
1190    }
1191
1192    /// Add an inherits edge (class extends class).
1193    pub fn add_inherits_edge(&mut self, child: NodeId, parent: NodeId) {
1194        self.staging
1195            .add_edge(child, parent, EdgeKind::Inherits, self.file_id);
1196    }
1197
1198    /// Add a contains edge (parent contains child, e.g., class contains method).
1199    pub fn add_contains_edge(&mut self, parent: NodeId, child: NodeId) {
1200        self.staging
1201            .add_edge(parent, child, EdgeKind::Contains, self.file_id);
1202    }
1203
1204    /// Add a WebAssembly call edge.
1205    ///
1206    /// Used when JavaScript/TypeScript code instantiates or calls WebAssembly modules:
1207    /// - `WebAssembly.instantiate()` / `WebAssembly.instantiateStreaming()`
1208    /// - `new WebAssembly.Module()` / `new WebAssembly.Instance()`
1209    /// - Calling exported WASM functions
1210    pub fn add_webassembly_edge(&mut self, caller: NodeId, wasm_target: NodeId) {
1211        self.staging
1212            .add_edge(caller, wasm_target, EdgeKind::WebAssemblyCall, self.file_id);
1213    }
1214
1215    /// Add an FFI call edge with the specified calling convention.
1216    ///
1217    /// Used for foreign function interface calls:
1218    /// - Node.js native addons (`.node` files)
1219    /// - ctypes/cffi in Python
1220    /// - JNI in Java
1221    /// - P/Invoke in C#
1222    pub fn add_ffi_edge(&mut self, caller: NodeId, ffi_target: NodeId, convention: FfiConvention) {
1223        self.staging.add_edge(
1224            caller,
1225            ffi_target,
1226            EdgeKind::FfiCall { convention },
1227            self.file_id,
1228        );
1229    }
1230
1231    /// Add an HTTP request edge.
1232    ///
1233    /// Use this when detecting HTTP calls like `fetch()` or `axios.get()`.
1234    pub fn add_http_request_edge(
1235        &mut self,
1236        caller: NodeId,
1237        target: NodeId,
1238        method: HttpMethod,
1239        url: Option<&str>,
1240    ) {
1241        let url_id = url.map(|value| self.intern(value));
1242        self.staging.add_edge(
1243            caller,
1244            target,
1245            EdgeKind::HttpRequest {
1246                method,
1247                url: url_id,
1248            },
1249            self.file_id,
1250        );
1251    }
1252
1253    /// Ensure a function node exists, creating it if needed.
1254    ///
1255    /// This is a convenience method that matches the legacy `ensure_function_node` pattern.
1256    pub fn ensure_function(
1257        &mut self,
1258        qualified_name: &str,
1259        span: Option<Span>,
1260        is_async: bool,
1261        is_unsafe: bool,
1262    ) -> NodeId {
1263        self.add_function(qualified_name, span, is_async, is_unsafe)
1264    }
1265
1266    /// Ensure a method node exists, creating it if needed.
1267    pub fn ensure_method(
1268        &mut self,
1269        qualified_name: &str,
1270        span: Option<Span>,
1271        is_async: bool,
1272        is_static: bool,
1273    ) -> NodeId {
1274        self.add_method(qualified_name, span, is_async, is_static)
1275    }
1276
1277    /// Get statistics about what's been staged.
1278    #[must_use]
1279    pub fn stats(&self) -> HelperStats {
1280        let staging_stats = self.staging.stats();
1281        HelperStats {
1282            strings_interned: self.string_cache.len(),
1283            nodes_created: self.node_cache.len(),
1284            nodes_staged: staging_stats.nodes_staged,
1285            edges_staged: staging_stats.edges_staged,
1286        }
1287    }
1288}
1289
1290fn semantic_name_for_node_input(original: &str, canonical: &str) -> String {
1291    if original.contains('/') {
1292        return original.to_string();
1293    }
1294
1295    canonical
1296        .rsplit("::")
1297        .next()
1298        .map_or_else(|| original.to_string(), ToString::to_string)
1299}
1300
1301/// Statistics from `GraphBuildHelper` operations.
1302#[derive(Debug, Clone, Default)]
1303pub struct HelperStats {
1304    /// Number of unique strings interned.
1305    pub strings_interned: usize,
1306    /// Number of unique nodes created.
1307    pub nodes_created: usize,
1308    /// Total nodes staged (from `StagingGraph`).
1309    pub nodes_staged: usize,
1310    /// Total edges staged (from `StagingGraph`).
1311    pub edges_staged: usize,
1312}
1313
1314#[cfg(test)]
1315mod tests {
1316    use super::*;
1317    use crate::graph::node::Position;
1318    use crate::graph::unified::build::staging::StagingOp;
1319    use std::path::PathBuf;
1320
1321    #[test]
1322    fn test_helper_add_function() {
1323        let mut staging = StagingGraph::new();
1324        let file = PathBuf::from("test.rs");
1325        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Rust);
1326
1327        let node_id = helper.add_function("main", None, false, false);
1328        assert!(!node_id.is_invalid());
1329        assert_eq!(helper.stats().nodes_created, 1);
1330    }
1331
1332    #[test]
1333    fn test_helper_deduplication() {
1334        let mut staging = StagingGraph::new();
1335        let file = PathBuf::from("test.rs");
1336        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Rust);
1337
1338        let id1 = helper.add_function("main", None, false, false);
1339        let id2 = helper.add_function("main", None, false, false);
1340
1341        assert_eq!(id1, id2, "Same function should return same NodeId");
1342        assert_eq!(
1343            helper.stats().nodes_created,
1344            1,
1345            "Should only create one node"
1346        );
1347    }
1348
1349    #[test]
1350    fn test_helper_string_interning() {
1351        let mut staging = StagingGraph::new();
1352        let file = PathBuf::from("test.rs");
1353        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Rust);
1354
1355        let s1 = helper.intern("hello");
1356        let s2 = helper.intern("world");
1357        let s3 = helper.intern("hello"); // Duplicate
1358
1359        assert_ne!(s1, s2, "Different strings should have different IDs");
1360        assert_eq!(s1, s3, "Same string should return same ID");
1361        assert_eq!(helper.stats().strings_interned, 2);
1362    }
1363
1364    #[test]
1365    fn test_helper_add_call_edge() {
1366        let mut staging = StagingGraph::new();
1367        let file = PathBuf::from("test.rs");
1368        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Rust);
1369
1370        let main_id = helper.add_function("main", None, false, false);
1371        let helper_id = helper.add_function("helper", None, false, false);
1372
1373        helper.add_call_edge(main_id, helper_id);
1374
1375        assert_eq!(helper.stats().edges_staged, 1);
1376        let edge_kind = staging.operations().iter().find_map(|op| {
1377            if let StagingOp::AddEdge { kind, .. } = op {
1378                Some(kind)
1379            } else {
1380                None
1381            }
1382        });
1383        match edge_kind {
1384            Some(EdgeKind::Calls {
1385                argument_count,
1386                is_async,
1387            }) => {
1388                assert_eq!(*argument_count, 255);
1389                assert!(!*is_async);
1390            }
1391            _ => panic!("Expected Calls edge"),
1392        }
1393    }
1394
1395    #[test]
1396    fn test_helper_multiple_node_kinds() {
1397        let mut staging = StagingGraph::new();
1398        let file = PathBuf::from("test.py");
1399        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Python);
1400
1401        let _class_id = helper.add_class("MyClass", None);
1402        let _method_id = helper.add_method("MyClass.my_method", None, false, false);
1403        let _func_id = helper.add_function("standalone_func", None, true, false);
1404
1405        assert_eq!(helper.stats().nodes_created, 3);
1406    }
1407
1408    #[test]
1409    fn test_helper_canonicalizes_language_native_qualified_names() {
1410        let mut staging = StagingGraph::new();
1411        let file = PathBuf::from("test.py");
1412        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Python);
1413
1414        let _method_id = helper.add_method("pkg.module.run", None, false, false);
1415
1416        let add_node_op = staging
1417            .operations()
1418            .iter()
1419            .find(|op| matches!(op, StagingOp::AddNode { .. }))
1420            .expect("Expected AddNode operation");
1421
1422        if let StagingOp::AddNode { entry, .. } = add_node_op {
1423            assert_eq!(staging.resolve_local_string(entry.name), Some("run"));
1424            assert_eq!(
1425                staging.resolve_node_name(entry),
1426                Some("pkg::module::run"),
1427                "expected GraphBuildHelper to canonicalize Python dotted qualified names"
1428            );
1429        }
1430    }
1431
1432    #[test]
1433    fn test_helper_preserves_path_qualified_names() {
1434        let mut staging = StagingGraph::new();
1435        let file = PathBuf::from("test.js");
1436        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::JavaScript);
1437
1438        let _func_id = helper.add_function("frontend/api.js::fetchUsers", None, false, false);
1439
1440        let add_node_op = staging
1441            .operations()
1442            .iter()
1443            .find(|op| matches!(op, StagingOp::AddNode { .. }))
1444            .expect("Expected AddNode operation");
1445
1446        if let StagingOp::AddNode { entry, .. } = add_node_op {
1447            assert_eq!(
1448                staging.resolve_local_string(entry.name),
1449                Some("frontend/api.js::fetchUsers")
1450            );
1451            assert_eq!(
1452                staging.resolve_node_name(entry),
1453                Some("frontend/api.js::fetchUsers"),
1454                "expected path-qualified names to remain unchanged"
1455            );
1456        }
1457    }
1458
1459    #[test]
1460    fn test_helper_verbatim_import_preserves_resource_name() {
1461        let mut staging = StagingGraph::new();
1462        let file = PathBuf::from("index.html");
1463        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Html);
1464
1465        let _import_id = helper.add_verbatim_import("styles.css", None);
1466
1467        let add_node_op = staging
1468            .operations()
1469            .iter()
1470            .find(|op| matches!(op, StagingOp::AddNode { .. }))
1471            .expect("Expected AddNode operation");
1472
1473        if let StagingOp::AddNode { entry, .. } = add_node_op {
1474            assert_eq!(staging.resolve_local_string(entry.name), Some("styles.css"));
1475            assert_eq!(entry.qualified_name, None);
1476            assert_eq!(
1477                staging.resolve_node_name(entry),
1478                Some("styles.css"),
1479                "expected verbatim resource imports to preserve their literal identity"
1480            );
1481        }
1482    }
1483
1484    #[test]
1485    fn test_helper_verbatim_variable_preserves_resource_name() {
1486        let mut staging = StagingGraph::new();
1487        let file = PathBuf::from("index.html");
1488        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Html);
1489
1490        let _variable_id = helper.add_verbatim_variable("/assets/logo.icon.png", None);
1491
1492        let add_node_op = staging
1493            .operations()
1494            .iter()
1495            .find(|op| matches!(op, StagingOp::AddNode { .. }))
1496            .expect("Expected AddNode operation");
1497
1498        if let StagingOp::AddNode { entry, .. } = add_node_op {
1499            assert_eq!(
1500                staging.resolve_local_string(entry.name),
1501                Some("/assets/logo.icon.png")
1502            );
1503            assert_eq!(entry.qualified_name, None);
1504            assert_eq!(
1505                staging.resolve_node_name(entry),
1506                Some("/assets/logo.icon.png"),
1507                "expected verbatim resource variables to preserve their literal identity"
1508            );
1509        }
1510    }
1511
1512    #[test]
1513    fn test_helper_ensure_function() {
1514        let mut staging = StagingGraph::new();
1515        let file = PathBuf::from("test.rs");
1516        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Rust);
1517
1518        let id1 = helper.ensure_function("foo", None, false, false);
1519        let id2 = helper.ensure_function("foo", None, true, false); // Different attrs, same name
1520
1521        assert_eq!(id1, id2, "ensure_function should be idempotent by name");
1522    }
1523
1524    #[test]
1525    fn test_helper_with_span() {
1526        let mut staging = StagingGraph::new();
1527        let file = PathBuf::from("test.rs");
1528        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Rust);
1529
1530        let span = Span {
1531            start: Position {
1532                line: 10,
1533                column: 0,
1534            },
1535            end: Position {
1536                line: 15,
1537                column: 1,
1538            },
1539        };
1540
1541        let node_id = helper.add_function("main", Some(span), false, false);
1542        assert!(!node_id.is_invalid());
1543    }
1544
1545    #[test]
1546    fn test_helper_add_call_edge_full() {
1547        let mut staging = StagingGraph::new();
1548        let file = PathBuf::from("test.rs");
1549        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Rust);
1550
1551        let caller_id = helper.add_function("caller", None, false, false);
1552        let callee_id = helper.add_function("callee", None, false, false);
1553
1554        // Add a call with specific metadata
1555        helper.add_call_edge_full(caller_id, callee_id, 3, true);
1556
1557        assert_eq!(helper.stats().edges_staged, 1);
1558
1559        // Verify the edge has correct metadata
1560        let edges = staging.operations();
1561        let call_edge = edges.iter().find(|op| {
1562            matches!(
1563                op,
1564                StagingOp::AddEdge {
1565                    kind: EdgeKind::Calls { .. },
1566                    ..
1567                }
1568            )
1569        });
1570
1571        assert!(call_edge.is_some());
1572        if let StagingOp::AddEdge {
1573            kind:
1574                EdgeKind::Calls {
1575                    argument_count,
1576                    is_async,
1577                },
1578            ..
1579        } = call_edge.unwrap()
1580        {
1581            assert_eq!(*argument_count, 3);
1582            assert!(*is_async);
1583        }
1584    }
1585
1586    #[test]
1587    fn test_helper_add_import_edge_full() {
1588        let mut staging = StagingGraph::new();
1589        let file = PathBuf::from("test.js");
1590        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::JavaScript);
1591
1592        let module_id = helper.add_module("app", None);
1593        let imported_id = helper.add_function("utils", None, false, false);
1594
1595        // Import with alias
1596        helper.add_import_edge_full(module_id, imported_id, Some("helpers"), false);
1597
1598        assert_eq!(helper.stats().edges_staged, 1);
1599
1600        // Verify the edge has correct metadata
1601        let edges = staging.operations();
1602        let import_edge = edges.iter().find(|op| {
1603            matches!(
1604                op,
1605                StagingOp::AddEdge {
1606                    kind: EdgeKind::Imports { .. },
1607                    ..
1608                }
1609            )
1610        });
1611
1612        assert!(import_edge.is_some());
1613        if let StagingOp::AddEdge {
1614            kind: EdgeKind::Imports { alias, is_wildcard },
1615            ..
1616        } = import_edge.unwrap()
1617        {
1618            assert!(alias.is_some(), "Alias should be present");
1619            assert!(!*is_wildcard);
1620        }
1621    }
1622
1623    #[test]
1624    fn test_helper_add_import_edge_wildcard() {
1625        let mut staging = StagingGraph::new();
1626        let file = PathBuf::from("test.js");
1627        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::JavaScript);
1628
1629        let module_id = helper.add_module("app", None);
1630        let imported_id = helper.add_module("lodash", None);
1631
1632        // Wildcard import: import * from "lodash"
1633        helper.add_import_edge_full(module_id, imported_id, None, true);
1634
1635        let edges = staging.operations();
1636        let import_edge = edges.iter().find(|op| {
1637            matches!(
1638                op,
1639                StagingOp::AddEdge {
1640                    kind: EdgeKind::Imports { .. },
1641                    ..
1642                }
1643            )
1644        });
1645
1646        if let StagingOp::AddEdge {
1647            kind: EdgeKind::Imports { alias, is_wildcard },
1648            ..
1649        } = import_edge.unwrap()
1650        {
1651            assert!(alias.is_none());
1652            assert!(*is_wildcard);
1653        }
1654    }
1655
1656    #[test]
1657    fn test_helper_add_export_edge_full() {
1658        let mut staging = StagingGraph::new();
1659        let file = PathBuf::from("test.js");
1660        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::JavaScript);
1661
1662        let module_id = helper.add_module("app", None);
1663        let component_id = helper.add_class("MyComponent", None);
1664
1665        // Default export
1666        helper.add_export_edge_full(module_id, component_id, ExportKind::Default, None);
1667
1668        assert_eq!(helper.stats().edges_staged, 1);
1669
1670        let edges = staging.operations();
1671        let export_edge = edges.iter().find(|op| {
1672            matches!(
1673                op,
1674                StagingOp::AddEdge {
1675                    kind: EdgeKind::Exports { .. },
1676                    ..
1677                }
1678            )
1679        });
1680
1681        assert!(export_edge.is_some());
1682        if let StagingOp::AddEdge {
1683            kind: EdgeKind::Exports { kind, alias },
1684            ..
1685        } = export_edge.unwrap()
1686        {
1687            assert_eq!(*kind, ExportKind::Default);
1688            assert!(alias.is_none());
1689        }
1690    }
1691
1692    #[test]
1693    fn test_helper_add_export_edge_with_alias() {
1694        let mut staging = StagingGraph::new();
1695        let file = PathBuf::from("test.js");
1696        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::JavaScript);
1697
1698        let module_id = helper.add_module("app", None);
1699        let helper_fn_id = helper.add_function("internalHelper", None, false, false);
1700
1701        // export { internalHelper as helper }
1702        helper.add_export_edge_full(module_id, helper_fn_id, ExportKind::Direct, Some("helper"));
1703
1704        let edges = staging.operations();
1705        let export_edge = edges.iter().find(|op| {
1706            matches!(
1707                op,
1708                StagingOp::AddEdge {
1709                    kind: EdgeKind::Exports { .. },
1710                    ..
1711                }
1712            )
1713        });
1714
1715        if let StagingOp::AddEdge {
1716            kind: EdgeKind::Exports { kind, alias },
1717            ..
1718        } = export_edge.unwrap()
1719        {
1720            assert_eq!(*kind, ExportKind::Direct);
1721            assert!(alias.is_some(), "Alias should be present");
1722        }
1723    }
1724
1725    #[test]
1726    fn test_helper_add_export_edge_reexport() {
1727        let mut staging = StagingGraph::new();
1728        let file = PathBuf::from("index.js");
1729        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::JavaScript);
1730
1731        let module_id = helper.add_module("index", None);
1732        let utils_id = helper.add_module("utils", None);
1733
1734        // export * as utils from "./utils"
1735        helper.add_export_edge_full(module_id, utils_id, ExportKind::Namespace, Some("utils"));
1736
1737        let edges = staging.operations();
1738        let export_edge = edges.iter().find(|op| {
1739            matches!(
1740                op,
1741                StagingOp::AddEdge {
1742                    kind: EdgeKind::Exports { .. },
1743                    ..
1744                }
1745            )
1746        });
1747
1748        if let StagingOp::AddEdge {
1749            kind: EdgeKind::Exports { kind, alias },
1750            ..
1751        } = export_edge.unwrap()
1752        {
1753            assert_eq!(*kind, ExportKind::Namespace);
1754            assert!(alias.is_some());
1755        }
1756    }
1757
1758    #[test]
1759    fn test_helper_add_call_edge_full_with_span() {
1760        let mut staging = StagingGraph::new();
1761        let file = PathBuf::from("test.rs");
1762        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Rust);
1763
1764        let caller_id = helper.add_function("caller", None, false, false);
1765        let callee_id = helper.add_function("callee", None, false, false);
1766
1767        let span = Span {
1768            start: Position { line: 5, column: 4 },
1769            end: Position {
1770                line: 5,
1771                column: 20,
1772            },
1773        };
1774
1775        helper.add_call_edge_full_with_span(caller_id, callee_id, 2, false, vec![span]);
1776
1777        let edges = staging.operations();
1778        let call_edge = edges.iter().find(|op| {
1779            matches!(
1780                op,
1781                StagingOp::AddEdge {
1782                    kind: EdgeKind::Calls { .. },
1783                    ..
1784                }
1785            )
1786        });
1787
1788        if let StagingOp::AddEdge {
1789            kind:
1790                EdgeKind::Calls {
1791                    argument_count,
1792                    is_async,
1793                },
1794            spans: edge_spans,
1795            ..
1796        } = call_edge.unwrap()
1797        {
1798            assert_eq!(*argument_count, 2);
1799            assert!(!*is_async);
1800            assert!(!edge_spans.is_empty());
1801        }
1802    }
1803
1804    #[test]
1805    fn test_helper_add_function_with_async_attribute() {
1806        let mut staging = StagingGraph::new();
1807        let file = PathBuf::from("test.kt");
1808        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Kotlin);
1809
1810        // Add an async (suspend) function
1811        let _func_id = helper.add_function("fetchData", None, true, false);
1812
1813        // Verify the staged node has is_async = true
1814        let ops = staging.operations();
1815        let add_node_op = ops
1816            .iter()
1817            .find(|op| matches!(op, StagingOp::AddNode { .. }));
1818
1819        assert!(add_node_op.is_some(), "Expected AddNode operation");
1820        if let StagingOp::AddNode { entry, .. } = add_node_op.unwrap() {
1821            assert!(
1822                entry.is_async,
1823                "Expected is_async=true for suspend function, got is_async=false"
1824            );
1825        }
1826    }
1827
1828    #[test]
1829    fn test_helper_add_method_with_static_attribute() {
1830        let mut staging = StagingGraph::new();
1831        let file = PathBuf::from("test.java");
1832        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Java);
1833
1834        // Add a static method
1835        let _method_id = helper.add_method("MyClass.staticMethod", None, false, true);
1836
1837        // Verify the staged node has is_static = true
1838        let ops = staging.operations();
1839        let add_node_op = ops
1840            .iter()
1841            .find(|op| matches!(op, StagingOp::AddNode { .. }));
1842
1843        assert!(add_node_op.is_some(), "Expected AddNode operation");
1844        if let StagingOp::AddNode { entry, .. } = add_node_op.unwrap() {
1845            assert!(
1846                entry.is_static,
1847                "Expected is_static=true for static method, got is_static=false"
1848            );
1849        }
1850    }
1851
1852    #[test]
1853    fn test_helper_add_function_without_attributes() {
1854        let mut staging = StagingGraph::new();
1855        let file = PathBuf::from("test.rs");
1856        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Rust);
1857
1858        // Add a regular function (not async, not unsafe)
1859        let _func_id = helper.add_function("regular_function", None, false, false);
1860
1861        // Verify the staged node has is_async = false
1862        let ops = staging.operations();
1863        let add_node_op = ops
1864            .iter()
1865            .find(|op| matches!(op, StagingOp::AddNode { .. }));
1866
1867        assert!(add_node_op.is_some(), "Expected AddNode operation");
1868        if let StagingOp::AddNode { entry, .. } = add_node_op.unwrap() {
1869            assert!(
1870                !entry.is_async,
1871                "Expected is_async=false for regular function"
1872            );
1873            assert!(
1874                !entry.is_static,
1875                "Expected is_static=false for regular function"
1876            );
1877        }
1878    }
1879
1880    #[test]
1881    fn test_helper_add_method_with_both_attributes() {
1882        let mut staging = StagingGraph::new();
1883        let file = PathBuf::from("test.kt");
1884        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Kotlin);
1885
1886        // Add an async static method
1887        let _method_id = helper.add_method("Service.asyncStaticMethod", None, true, true);
1888
1889        // Verify the staged node has both flags set
1890        let ops = staging.operations();
1891        let add_node_op = ops
1892            .iter()
1893            .find(|op| matches!(op, StagingOp::AddNode { .. }));
1894
1895        assert!(add_node_op.is_some(), "Expected AddNode operation");
1896        if let StagingOp::AddNode { entry, .. } = add_node_op.unwrap() {
1897            assert!(entry.is_async, "Expected is_async=true for async method");
1898            assert!(entry.is_static, "Expected is_static=true for static method");
1899        }
1900    }
1901
1902    #[test]
1903    fn test_helper_add_function_with_unsafe_attribute() {
1904        let mut staging = StagingGraph::new();
1905        let file = PathBuf::from("test.rs");
1906        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Rust);
1907
1908        // Add an unsafe function
1909        let _func_id = helper.add_function("unsafe_function", None, false, true);
1910
1911        // Verify the staged node has is_unsafe = true
1912        let ops = staging.operations();
1913        let add_node_op = ops
1914            .iter()
1915            .find(|op| matches!(op, StagingOp::AddNode { .. }));
1916
1917        assert!(add_node_op.is_some(), "Expected AddNode operation");
1918        if let StagingOp::AddNode { entry, .. } = add_node_op.unwrap() {
1919            assert!(
1920                entry.is_unsafe,
1921                "Expected is_unsafe=true for unsafe function, got is_unsafe={}",
1922                entry.is_unsafe
1923            );
1924        }
1925    }
1926}