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