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/// Node kinds that represent callable targets and may be used interchangeably
55/// across files. When a plugin calls `ensure_function` for a name that already
56/// exists as any of these kinds, the existing node is reused instead of creating
57/// a duplicate spanless stub.
58///
59/// dec44131f established this for the Method<->Function pair. This const
60/// generalizes it to all call-compatible kinds.
61pub(crate) const CALL_COMPATIBLE_KINDS: &[NodeKind] = &[
62    NodeKind::Function,
63    NodeKind::Method,
64    NodeKind::Macro,
65    NodeKind::Constant,
66    NodeKind::LambdaTarget,
67];
68
69/// Hint for the kind of callee node to create when no cached node exists.
70///
71/// Only call-compatible kinds are valid hints. Using a non-call-compatible
72/// kind (e.g., `StyleRule`) is prevented at compile time by this enum.
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74pub enum CalleeKindHint {
75    /// Default: create a `Function` node.
76    Function,
77    /// Create a `Method` node (receiver method).
78    Method,
79    /// Create a `Macro` node (C preprocessor macro, Rust macro, etc.).
80    Macro,
81    /// Create a `Constant` node (function pointer constant).
82    Constant,
83    /// Create a `LambdaTarget` node (Java SAM interface, Kotlin lambda, etc.).
84    LambdaTarget,
85    /// No preference: create a `Function` node (same as `Function`).
86    Any,
87}
88
89impl CalleeKindHint {
90    /// Convert to the default `NodeKind` for node creation.
91    fn to_node_kind(self) -> NodeKind {
92        match self {
93            Self::Function | Self::Any => NodeKind::Function,
94            Self::Method => NodeKind::Method,
95            Self::Macro => NodeKind::Macro,
96            Self::Constant => NodeKind::Constant,
97            Self::LambdaTarget => NodeKind::LambdaTarget,
98        }
99    }
100}
101
102/// Helper for building graphs in `GraphBuilder` implementations.
103///
104/// Provides high-level abstractions over `StagingGraph` that handle:
105/// - String interning with local ID tracking
106/// - Qualified name deduplication
107/// - Node and edge creation with proper types
108#[derive(Debug)]
109pub struct GraphBuildHelper<'a> {
110    /// The underlying staging graph.
111    staging: &'a mut StagingGraph,
112    /// Language for this file.
113    language: Language,
114    /// File ID (pre-allocated).
115    file_id: FileId,
116    /// File path for error messages.
117    file_path: String,
118    /// Local string interning: string value -> local `StringId`.
119    string_cache: HashMap<String, StringId>,
120    /// Next local string ID to allocate.
121    next_string_id: u32,
122    /// Qualified name -> `NodeId` mapping for deduplication.
123    ///
124    /// Shared by both canonical nodes (via `add_node_internal`, which stores
125    /// under the **canonicalized** qualified name) and verbatim nodes (via
126    /// `add_node_verbatim`, which stores under the **raw** name).  Collisions
127    /// are avoided because canonical names never contain native delimiters
128    /// (e.g. `.`, `#`) while verbatim names preserve them (e.g. `styles.css`).
129    node_cache: HashMap<(String, NodeKind), NodeId>,
130}
131
132impl<'a> GraphBuildHelper<'a> {
133    /// Create a new helper for the given staging graph and file.
134    ///
135    /// The `file_id` should be pre-allocated by the caller (typically 0 for
136    /// per-file staging buffers).
137    pub fn new(staging: &'a mut StagingGraph, file: &Path, language: Language) -> Self {
138        Self {
139            staging,
140            language,
141            file_id: FileId::new(0), // Per-file staging uses local file ID 0
142            file_path: file.display().to_string(),
143            string_cache: HashMap::new(),
144            next_string_id: 0,
145            node_cache: HashMap::new(),
146        }
147    }
148
149    /// Create a helper with a specific file ID.
150    pub fn with_file_id(
151        staging: &'a mut StagingGraph,
152        file: &Path,
153        language: Language,
154        file_id: FileId,
155    ) -> Self {
156        Self {
157            staging,
158            language,
159            file_id,
160            file_path: file.display().to_string(),
161            string_cache: HashMap::new(),
162            next_string_id: 0,
163            node_cache: HashMap::new(),
164        }
165    }
166
167    /// Get the language for this helper.
168    #[must_use]
169    pub fn language(&self) -> Language {
170        self.language
171    }
172
173    /// Get the file ID for this helper.
174    #[must_use]
175    pub fn file_id(&self) -> FileId {
176        self.file_id
177    }
178
179    /// Look up a node ID by its qualified name and kind from the internal cache.
180    ///
181    /// Returns the `NodeId` if a node with the given `(name, kind)` pair was
182    /// previously created through this helper. This is used by macro boundary
183    /// analysis to find graph nodes corresponding to AST items.
184    #[must_use]
185    pub fn lookup_node(&self, name: &str, kind: NodeKind) -> Option<NodeId> {
186        self.node_cache.get(&(name.to_string(), kind)).copied()
187    }
188
189    /// Get the file path.
190    #[must_use]
191    pub fn file_path(&self) -> &str {
192        &self.file_path
193    }
194
195    /// Attach body hashes to all staged nodes using the given content bytes.
196    ///
197    /// Multi-language plugins (Vue, Svelte) should call this per extracted
198    /// script block so that node body spans — which are relative to the
199    /// block content, not the full SFC file — produce correct hashes.
200    /// Nodes that already have a hash are skipped, so the later whole-file
201    /// call in the indexing entrypoint is harmless.
202    pub fn attach_body_hashes(&mut self, content: &[u8]) {
203        self.staging.attach_body_hashes(content);
204    }
205
206    /// Intern a string and get a local `StringId`.
207    ///
208    /// Strings are deduplicated: calling with the same value returns the same ID.
209    /// The local `StringId` is passed to the staging graph so that during
210    /// `commit_strings()`, a remap table from local to global IDs can be built.
211    pub fn intern(&mut self, s: &str) -> StringId {
212        if let Some(&id) = self.string_cache.get(s) {
213            return id;
214        }
215
216        let id = StringId::new_local(self.next_string_id);
217        self.next_string_id += 1;
218        self.string_cache.insert(s.to_string(), id);
219        // Pass the local_id to staging so it can build the remap table during commit
220        self.staging.intern_string(id, s.to_string());
221        id
222    }
223
224    /// Check if a node with the given qualified name already exists.
225    #[must_use]
226    pub fn has_node(&self, qualified_name: &str) -> bool {
227        self.node_cache
228            .keys()
229            .any(|(name, _)| name == qualified_name)
230    }
231
232    /// Get an existing node by qualified name.
233    #[must_use]
234    pub fn get_node(&self, qualified_name: &str) -> Option<NodeId> {
235        self.node_cache
236            .iter()
237            .find_map(|((name, _), id)| (name == qualified_name).then_some(*id))
238    }
239
240    /// Check if a node with the given qualified name and kind already exists.
241    #[must_use]
242    pub fn has_node_with_kind(&self, qualified_name: &str, kind: NodeKind) -> bool {
243        self.node_cache
244            .contains_key(&(qualified_name.to_string(), kind))
245    }
246
247    /// Get an existing node by qualified name and kind.
248    #[must_use]
249    pub fn get_node_with_kind(&self, qualified_name: &str, kind: NodeKind) -> Option<NodeId> {
250        self.node_cache
251            .get(&(qualified_name.to_string(), kind))
252            .copied()
253    }
254
255    /// Add a function node with the given qualified name.
256    ///
257    /// Returns the `NodeId` (creating the node if it doesn't exist).
258    pub fn add_function(
259        &mut self,
260        qualified_name: &str,
261        span: Option<Span>,
262        is_async: bool,
263        is_unsafe: bool,
264    ) -> NodeId {
265        self.add_node_internal(
266            qualified_name,
267            span,
268            NodeKind::Function,
269            &[("async", is_async), ("unsafe", is_unsafe)],
270            None,
271            None,
272        )
273    }
274
275    /// Add a function node with visibility.
276    ///
277    /// Returns the `NodeId` (creating the node if it doesn't exist).
278    pub fn add_function_with_visibility(
279        &mut self,
280        qualified_name: &str,
281        span: Option<Span>,
282        is_async: bool,
283        is_unsafe: bool,
284        visibility: Option<&str>,
285    ) -> NodeId {
286        self.add_node_internal(
287            qualified_name,
288            span,
289            NodeKind::Function,
290            &[("async", is_async), ("unsafe", is_unsafe)],
291            visibility,
292            None,
293        )
294    }
295
296    /// Add a function node with signature (return type).
297    ///
298    /// The signature is used for `returns:` queries.
299    /// Returns the `NodeId` (creating the node if it doesn't exist).
300    pub fn add_function_with_signature(
301        &mut self,
302        qualified_name: &str,
303        span: Option<Span>,
304        is_async: bool,
305        is_unsafe: bool,
306        visibility: Option<&str>,
307        signature: Option<&str>,
308    ) -> NodeId {
309        self.add_node_internal(
310            qualified_name,
311            span,
312            NodeKind::Function,
313            &[("async", is_async), ("unsafe", is_unsafe)],
314            visibility,
315            signature,
316        )
317    }
318
319    /// Add a method node with the given qualified name.
320    pub fn add_method(
321        &mut self,
322        qualified_name: &str,
323        span: Option<Span>,
324        is_async: bool,
325        is_static: bool,
326    ) -> NodeId {
327        self.add_node_internal(
328            qualified_name,
329            span,
330            NodeKind::Method,
331            &[("async", is_async), ("static", is_static)],
332            None,
333            None,
334        )
335    }
336
337    /// Add a method node with visibility.
338    pub fn add_method_with_visibility(
339        &mut self,
340        qualified_name: &str,
341        span: Option<Span>,
342        is_async: bool,
343        is_static: bool,
344        visibility: Option<&str>,
345    ) -> NodeId {
346        self.add_node_internal(
347            qualified_name,
348            span,
349            NodeKind::Method,
350            &[("async", is_async), ("static", is_static)],
351            visibility,
352            None,
353        )
354    }
355
356    /// Add a method node with signature (return type).
357    ///
358    /// The signature is used for `returns:` queries.
359    /// Returns the `NodeId` (creating the node if it doesn't exist).
360    pub fn add_method_with_signature(
361        &mut self,
362        qualified_name: &str,
363        span: Option<Span>,
364        is_async: bool,
365        is_static: bool,
366        visibility: Option<&str>,
367        signature: Option<&str>,
368    ) -> NodeId {
369        self.add_node_internal(
370            qualified_name,
371            span,
372            NodeKind::Method,
373            &[("async", is_async), ("static", is_static)],
374            visibility,
375            signature,
376        )
377    }
378
379    /// Add a class node.
380    pub fn add_class(&mut self, qualified_name: &str, span: Option<Span>) -> NodeId {
381        self.add_node_internal(qualified_name, span, NodeKind::Class, &[], None, None)
382    }
383
384    /// Add a class node with visibility.
385    pub fn add_class_with_visibility(
386        &mut self,
387        qualified_name: &str,
388        span: Option<Span>,
389        visibility: Option<&str>,
390    ) -> NodeId {
391        self.add_node_internal(qualified_name, span, NodeKind::Class, &[], visibility, None)
392    }
393
394    /// Add a struct node.
395    pub fn add_struct(&mut self, qualified_name: &str, span: Option<Span>) -> NodeId {
396        self.add_node_internal(qualified_name, span, NodeKind::Struct, &[], None, None)
397    }
398
399    /// Add a struct node with visibility.
400    pub fn add_struct_with_visibility(
401        &mut self,
402        qualified_name: &str,
403        span: Option<Span>,
404        visibility: Option<&str>,
405    ) -> NodeId {
406        self.add_node_internal(
407            qualified_name,
408            span,
409            NodeKind::Struct,
410            &[],
411            visibility,
412            None,
413        )
414    }
415
416    /// Add a module node.
417    pub fn add_module(&mut self, qualified_name: &str, span: Option<Span>) -> NodeId {
418        self.add_node_internal(qualified_name, span, NodeKind::Module, &[], None, None)
419    }
420
421    /// Add a resource node.
422    pub fn add_resource(&mut self, qualified_name: &str, span: Option<Span>) -> NodeId {
423        self.add_node_internal(qualified_name, span, NodeKind::Resource, &[], None, None)
424    }
425
426    /// Add an endpoint node for HTTP route handlers.
427    ///
428    /// The qualified name should follow the convention `route::{METHOD}::{path}`,
429    /// for example `route::GET::/api/users` or `route::POST::/api/items`.
430    ///
431    /// Endpoint nodes are used by Pass 5 (cross-language linking) to match
432    /// HTTP requests from client code to server-side route handlers.
433    pub fn add_endpoint(&mut self, qualified_name: &str, span: Option<Span>) -> NodeId {
434        self.add_node_internal(qualified_name, span, NodeKind::Endpoint, &[], None, None)
435    }
436
437    /// Add an import node.
438    pub fn add_import(&mut self, qualified_name: &str, span: Option<Span>) -> NodeId {
439        self.add_node_internal(qualified_name, span, NodeKind::Import, &[], None, None)
440    }
441
442    /// Add an import node while preserving the original path-like identifier.
443    ///
444    /// Use this for resource imports such as `styles.css`, `app.js`, or
445    /// similar asset filenames where `.` is part of the path rather than a
446    /// language-native qualified-name separator.
447    pub fn add_verbatim_import(&mut self, name: &str, span: Option<Span>) -> NodeId {
448        self.add_node_verbatim(name, span, NodeKind::Import, &[], None, None)
449    }
450
451    /// Add a variable node.
452    pub fn add_variable(&mut self, qualified_name: &str, span: Option<Span>) -> NodeId {
453        self.add_node_internal(qualified_name, span, NodeKind::Variable, &[], None, None)
454    }
455
456    /// Add a variable node while preserving the original identifier exactly.
457    ///
458    /// Use this for static asset references where the literal path is the
459    /// graph identity.
460    pub fn add_verbatim_variable(&mut self, name: &str, span: Option<Span>) -> NodeId {
461        self.add_node_verbatim(name, span, NodeKind::Variable, &[], None, None)
462    }
463
464    /// Add a constant node.
465    pub fn add_constant(&mut self, qualified_name: &str, span: Option<Span>) -> NodeId {
466        self.add_node_internal(qualified_name, span, NodeKind::Constant, &[], None, None)
467    }
468
469    /// Add a constant node with visibility.
470    pub fn add_constant_with_visibility(
471        &mut self,
472        qualified_name: &str,
473        span: Option<Span>,
474        visibility: Option<&str>,
475    ) -> NodeId {
476        self.add_node_internal(
477            qualified_name,
478            span,
479            NodeKind::Constant,
480            &[],
481            visibility,
482            None,
483        )
484    }
485
486    /// Add a constant node with static and visibility attributes.
487    pub fn add_constant_with_static_and_visibility(
488        &mut self,
489        qualified_name: &str,
490        span: Option<Span>,
491        is_static: bool,
492        visibility: Option<&str>,
493    ) -> NodeId {
494        let attrs: &[(&str, bool)] = if is_static { &[("static", true)] } else { &[] };
495        self.add_node_internal(
496            qualified_name,
497            span,
498            NodeKind::Constant,
499            attrs,
500            visibility,
501            None,
502        )
503    }
504
505    /// Add a property node with static and visibility attributes.
506    pub fn add_property_with_static_and_visibility(
507        &mut self,
508        qualified_name: &str,
509        span: Option<Span>,
510        is_static: bool,
511        visibility: Option<&str>,
512    ) -> NodeId {
513        let attrs: &[(&str, bool)] = if is_static { &[("static", true)] } else { &[] };
514        self.add_node_internal(
515            qualified_name,
516            span,
517            NodeKind::Property,
518            attrs,
519            visibility,
520            None,
521        )
522    }
523
524    /// Add an enum node.
525    pub fn add_enum(&mut self, qualified_name: &str, span: Option<Span>) -> NodeId {
526        self.add_node_internal(qualified_name, span, NodeKind::Enum, &[], None, None)
527    }
528
529    /// Add an enum node with visibility.
530    pub fn add_enum_with_visibility(
531        &mut self,
532        qualified_name: &str,
533        span: Option<Span>,
534        visibility: Option<&str>,
535    ) -> NodeId {
536        self.add_node_internal(qualified_name, span, NodeKind::Enum, &[], visibility, None)
537    }
538
539    /// Add an interface/trait node.
540    pub fn add_interface(&mut self, qualified_name: &str, span: Option<Span>) -> NodeId {
541        self.add_node_internal(qualified_name, span, NodeKind::Interface, &[], None, None)
542    }
543
544    /// Add an interface/trait node with visibility.
545    pub fn add_interface_with_visibility(
546        &mut self,
547        qualified_name: &str,
548        span: Option<Span>,
549        visibility: Option<&str>,
550    ) -> NodeId {
551        self.add_node_internal(
552            qualified_name,
553            span,
554            NodeKind::Interface,
555            &[],
556            visibility,
557            None,
558        )
559    }
560
561    /// Add a type alias node.
562    pub fn add_type(&mut self, qualified_name: &str, span: Option<Span>) -> NodeId {
563        self.add_node_internal(qualified_name, span, NodeKind::Type, &[], None, None)
564    }
565
566    /// Add a type alias node with visibility.
567    pub fn add_type_with_visibility(
568        &mut self,
569        qualified_name: &str,
570        span: Option<Span>,
571        visibility: Option<&str>,
572    ) -> NodeId {
573        self.add_node_internal(qualified_name, span, NodeKind::Type, &[], visibility, None)
574    }
575
576    /// Add a lifetime node.
577    pub fn add_lifetime(&mut self, qualified_name: &str, span: Option<Span>) -> NodeId {
578        self.add_node_internal(qualified_name, span, NodeKind::Lifetime, &[], None, None)
579    }
580
581    /// Add a lifetime constraint edge.
582    pub fn add_lifetime_constraint_edge(
583        &mut self,
584        source: NodeId,
585        target: NodeId,
586        constraint_kind: LifetimeConstraintKind,
587    ) {
588        self.staging.add_edge(
589            source,
590            target,
591            EdgeKind::LifetimeConstraint { constraint_kind },
592            self.file_id,
593        );
594    }
595
596    /// Add a trait method binding edge.
597    ///
598    /// This edge represents the resolution of a trait method call to a concrete
599    /// implementation.
600    pub fn add_trait_method_binding_edge(
601        &mut self,
602        caller: NodeId,
603        callee: NodeId,
604        trait_name: &str,
605        impl_type: &str,
606        is_ambiguous: bool,
607    ) {
608        let trait_name_id = self.intern(trait_name);
609        let impl_type_id = self.intern(impl_type);
610        self.staging.add_edge(
611            caller,
612            callee,
613            EdgeKind::TraitMethodBinding {
614                trait_name: trait_name_id,
615                impl_type: impl_type_id,
616                is_ambiguous,
617            },
618            self.file_id,
619        );
620    }
621
622    /// Add a macro expansion edge.
623    ///
624    /// Represents the expansion of a macro invocation to its generated code.
625    /// Only available when macro expansion is enabled.
626    ///
627    /// # Arguments
628    ///
629    /// * `invocation` - The macro invocation site node (e.g., derive attribute or macro call)
630    /// * `expansion` - The macro definition or generated code node
631    /// * `expansion_kind` - The kind of macro expansion (Derive, Attribute, Declarative, Function)
632    /// * `is_verified` - Whether the expansion has been verified (requires `cargo expand`)
633    ///
634    /// # Example
635    ///
636    /// ```ignore
637    /// // #[derive(Debug)] on a struct
638    /// let struct_id = helper.add_struct("MyStruct", Some(span));
639    /// let derive_macro_id = helper.add_node("MyStruct::derive_Debug", None, NodeKind::Macro);
640    /// helper.add_macro_expansion_edge(
641    ///     struct_id,
642    ///     derive_macro_id,
643    ///     MacroExpansionKind::Derive,
644    ///     false,
645    /// );
646    /// ```
647    pub fn add_macro_expansion_edge(
648        &mut self,
649        invocation: NodeId,
650        expansion: NodeId,
651        expansion_kind: MacroExpansionKind,
652        is_verified: bool,
653    ) {
654        self.staging.add_edge(
655            invocation,
656            expansion,
657            EdgeKind::MacroExpansion {
658                expansion_kind,
659                is_verified,
660            },
661            self.file_id,
662        );
663    }
664
665    /// Add a generic node with custom kind.
666    pub fn add_node(&mut self, qualified_name: &str, span: Option<Span>, kind: NodeKind) -> NodeId {
667        self.add_node_internal(qualified_name, span, kind, &[], None, None)
668    }
669
670    /// Add a generic node with visibility.
671    pub fn add_node_with_visibility(
672        &mut self,
673        qualified_name: &str,
674        span: Option<Span>,
675        kind: NodeKind,
676        visibility: Option<&str>,
677    ) -> NodeId {
678        self.add_node_internal(qualified_name, span, kind, &[], visibility, None)
679    }
680
681    /// Internal helper for adding nodes.
682    ///
683    /// Applies attributes to the node entry:
684    /// - `"async"` → `NodeEntry::with_async(true/false)`
685    /// - `"static"` → `NodeEntry::with_static(true/false)`
686    /// - `"unsafe"` → `NodeEntry::with_unsafe(true/false)`
687    ///
688    /// When `signature` is `Some`, the signature field is set on the node for
689    /// `returns:` queries.
690    fn add_node_internal(
691        &mut self,
692        qualified_name: &str,
693        span: Option<Span>,
694        kind: NodeKind,
695        attributes: &[(&str, bool)],
696        visibility: Option<&str>,
697        signature: Option<&str>,
698    ) -> NodeId {
699        let canonical_qualified_name =
700            canonicalize_graph_qualified_name(self.language, qualified_name);
701        let semantic_name = semantic_name_for_node_input(qualified_name, &canonical_qualified_name);
702        let mut is_async = false;
703        let mut is_static = false;
704        let mut is_unsafe = false;
705        for &(key, value) in attributes {
706            match key {
707                "async" => is_async |= value,
708                "static" => is_static |= value,
709                "unsafe" => is_unsafe |= value,
710                _ => {}
711            }
712        }
713
714        // Check cache first
715        if let Some(&id) = self
716            .node_cache
717            .get(&(canonical_qualified_name.clone(), kind))
718        {
719            let visibility_id = visibility.map(|vis| self.intern(vis));
720            let signature_id = signature.map(|sig| self.intern(sig));
721            self.staging.update_node_entry(
722                id,
723                span,
724                is_async,
725                is_static,
726                is_unsafe,
727                visibility_id,
728                signature_id,
729            );
730            return id;
731        }
732
733        let name_id = self.intern(&semantic_name);
734
735        // Create node entry
736        let mut entry = NodeEntry::new(kind, name_id, self.file_id);
737        if semantic_name != canonical_qualified_name {
738            let qualified_name_id = self.intern(&canonical_qualified_name);
739            entry = entry.with_qualified_name(qualified_name_id);
740        }
741
742        // Set span if provided
743        if let Some(s) = span {
744            let start_line = u32::try_from(s.start.line.saturating_add(1)).unwrap_or(u32::MAX);
745            let start_column = u32::try_from(s.start.column).unwrap_or(u32::MAX);
746            let end_line = u32::try_from(s.end.line.saturating_add(1)).unwrap_or(u32::MAX);
747            let end_column = u32::try_from(s.end.column).unwrap_or(u32::MAX);
748            entry = entry.with_location(start_line, start_column, end_line, end_column);
749        }
750
751        // Apply attributes to node entry
752        if is_async {
753            entry = entry.with_async(true);
754        }
755        if is_static {
756            entry = entry.with_static(true);
757        }
758        if is_unsafe {
759            entry = entry.with_unsafe(true);
760        }
761
762        // Apply visibility if provided
763        if let Some(vis) = visibility {
764            let vis_id = self.intern(vis);
765            entry = entry.with_visibility(vis_id);
766        }
767
768        // Apply signature (return type) if provided
769        if let Some(sig) = signature {
770            let sig_id = self.intern(sig);
771            entry = entry.with_signature(sig_id);
772        }
773
774        // Stage the node
775        let node_id = self.staging.add_node(entry);
776
777        // Cache for deduplication
778        self.node_cache
779            .insert((canonical_qualified_name, kind), node_id);
780
781        node_id
782    }
783
784    fn add_node_verbatim(
785        &mut self,
786        name: &str,
787        span: Option<Span>,
788        kind: NodeKind,
789        attributes: &[(&str, bool)],
790        visibility: Option<&str>,
791        signature: Option<&str>,
792    ) -> NodeId {
793        let mut is_async = false;
794        let mut is_static = false;
795        let mut is_unsafe = false;
796        for &(key, value) in attributes {
797            match key {
798                "async" => is_async |= value,
799                "static" => is_static |= value,
800                "unsafe" => is_unsafe |= value,
801                _ => {}
802            }
803        }
804
805        if let Some(&id) = self.node_cache.get(&(name.to_string(), kind)) {
806            let visibility_id = visibility.map(|vis| self.intern(vis));
807            let signature_id = signature.map(|sig| self.intern(sig));
808            self.staging.update_node_entry(
809                id,
810                span,
811                is_async,
812                is_static,
813                is_unsafe,
814                visibility_id,
815                signature_id,
816            );
817            return id;
818        }
819
820        let name_id = self.intern(name);
821        let mut entry = NodeEntry::new(kind, name_id, self.file_id);
822
823        if let Some(s) = span {
824            let start_line = u32::try_from(s.start.line.saturating_add(1)).unwrap_or(u32::MAX);
825            let start_column = u32::try_from(s.start.column).unwrap_or(u32::MAX);
826            let end_line = u32::try_from(s.end.line.saturating_add(1)).unwrap_or(u32::MAX);
827            let end_column = u32::try_from(s.end.column).unwrap_or(u32::MAX);
828            entry = entry.with_location(start_line, start_column, end_line, end_column);
829        }
830
831        if is_async {
832            entry = entry.with_async(true);
833        }
834        if is_static {
835            entry = entry.with_static(true);
836        }
837        if is_unsafe {
838            entry = entry.with_unsafe(true);
839        }
840
841        if let Some(vis) = visibility {
842            let vis_id = self.intern(vis);
843            entry = entry.with_visibility(vis_id);
844        }
845        if let Some(sig) = signature {
846            let sig_id = self.intern(sig);
847            entry = entry.with_signature(sig_id);
848        }
849
850        let node_id = self.staging.add_node(entry);
851        self.node_cache.insert((name.to_string(), kind), node_id);
852        node_id
853    }
854
855    /// Add a call edge from caller to callee.
856    pub fn add_call_edge(&mut self, caller: NodeId, callee: NodeId) {
857        self.add_call_edge_with_span(caller, callee, Vec::new());
858    }
859
860    /// Add a call edge from caller to callee with source span information.
861    ///
862    /// The span should point to the call site location in source code.
863    ///
864    /// # Note
865    ///
866    /// This method uses default metadata (`argument_count: 255` sentinel for unknown, `is_async: false`).
867    /// Use [`add_call_edge_full`](Self::add_call_edge_full) when you need to specify
868    /// argument count or async status explicitly.
869    pub fn add_call_edge_with_span(
870        &mut self,
871        caller: NodeId,
872        callee: NodeId,
873        spans: Vec<crate::graph::node::Span>,
874    ) {
875        self.staging.add_edge_with_spans(
876            caller,
877            callee,
878            EdgeKind::Calls {
879                argument_count: 255,
880                is_async: false,
881            },
882            self.file_id,
883            spans,
884        );
885    }
886
887    /// Add a call edge with full metadata.
888    ///
889    /// Use this method when you know the argument count or when the call is async.
890    /// For calls where metadata is unknown, use [`add_call_edge`](Self::add_call_edge)
891    /// which uses default values (`argument_count: 255` sentinel, `is_async: false`).
892    ///
893    /// # Arguments
894    ///
895    /// * `caller` - The node making the call
896    /// * `callee` - The node being called
897    /// * `argument_count` - Number of arguments in the call (0-254, use 255 for unknown)
898    /// * `is_async` - Whether this is an async/await call
899    ///
900    /// # Canonical Usage
901    ///
902    /// | Scenario | Method |
903    /// |----------|--------|
904    /// | Argument count known, sync call | `add_call_edge_full(caller, callee, arg_count, false)` |
905    /// | Argument count known, async call | `add_call_edge_full(caller, callee, arg_count, true)` |
906    /// | Argument count unknown, sync call | `add_call_edge(caller, callee)` or `add_call_edge_full(caller, callee, 255, false)` |
907    ///
908    /// # Example
909    ///
910    /// ```ignore
911    /// // Function call with 3 arguments
912    /// helper.add_call_edge_full(main_id, helper_id, 3, false);
913    ///
914    /// // Async call with 1 argument
915    /// helper.add_call_edge_full(main_id, async_fn_id, 1, true);
916    /// ```
917    pub fn add_call_edge_full(
918        &mut self,
919        caller: NodeId,
920        callee: NodeId,
921        argument_count: u8,
922        is_async: bool,
923    ) {
924        self.staging.add_edge(
925            caller,
926            callee,
927            EdgeKind::Calls {
928                argument_count,
929                is_async,
930            },
931            self.file_id,
932        );
933    }
934
935    /// Add a call edge with full metadata and source span information.
936    ///
937    /// Combines the functionality of [`add_call_edge_full`](Self::add_call_edge_full)
938    /// and span tracking.
939    pub fn add_call_edge_full_with_span(
940        &mut self,
941        caller: NodeId,
942        callee: NodeId,
943        argument_count: u8,
944        is_async: bool,
945        spans: Vec<crate::graph::node::Span>,
946    ) {
947        self.staging.add_edge_with_spans(
948            caller,
949            callee,
950            EdgeKind::Calls {
951                argument_count,
952                is_async,
953            },
954            self.file_id,
955            spans,
956        );
957    }
958
959    /// Add a database table read edge (SQL).
960    pub fn add_table_read_edge_with_span(
961        &mut self,
962        reader: NodeId,
963        table: NodeId,
964        table_name: &str,
965        schema: Option<&str>,
966        spans: Vec<crate::graph::node::Span>,
967    ) {
968        let table_name_id = self.intern(table_name);
969        let schema_id = schema.map(|s| self.intern(s));
970        self.staging.add_edge_with_spans(
971            reader,
972            table,
973            EdgeKind::TableRead {
974                table_name: table_name_id,
975                schema: schema_id,
976            },
977            self.file_id,
978            spans,
979        );
980    }
981
982    /// Add a database table write edge (SQL).
983    pub fn add_table_write_edge_with_span(
984        &mut self,
985        writer: NodeId,
986        table: NodeId,
987        table_name: &str,
988        schema: Option<&str>,
989        operation: TableWriteOp,
990        spans: Vec<crate::graph::node::Span>,
991    ) {
992        let table_name_id = self.intern(table_name);
993        let schema_id = schema.map(|s| self.intern(s));
994        self.staging.add_edge_with_spans(
995            writer,
996            table,
997            EdgeKind::TableWrite {
998                table_name: table_name_id,
999                schema: schema_id,
1000                operation,
1001            },
1002            self.file_id,
1003            spans,
1004        );
1005    }
1006
1007    /// Add a database trigger relationship edge (SQL).
1008    ///
1009    /// Convention: `trigger -> table` with `EdgeKind::TriggeredBy`.
1010    pub fn add_triggered_by_edge_with_span(
1011        &mut self,
1012        trigger: NodeId,
1013        table: NodeId,
1014        trigger_name: &str,
1015        schema: Option<&str>,
1016        spans: Vec<crate::graph::node::Span>,
1017    ) {
1018        let trigger_name_id = self.intern(trigger_name);
1019        let schema_id = schema.map(|s| self.intern(s));
1020        self.staging.add_edge_with_spans(
1021            trigger,
1022            table,
1023            EdgeKind::TriggeredBy {
1024                trigger_name: trigger_name_id,
1025                schema: schema_id,
1026            },
1027            self.file_id,
1028            spans,
1029        );
1030    }
1031
1032    /// Add an import edge from importer to imported module/symbol.
1033    ///
1034    /// This method uses default metadata (`alias: None`, `is_wildcard: false`).
1035    /// Use [`add_import_edge_full`](Self::add_import_edge_full) when importing
1036    /// with an alias or for wildcard imports.
1037    pub fn add_import_edge(&mut self, importer: NodeId, imported: NodeId) {
1038        self.staging.add_edge(
1039            importer,
1040            imported,
1041            EdgeKind::Imports {
1042                alias: None,
1043                is_wildcard: false,
1044            },
1045            self.file_id,
1046        );
1047    }
1048
1049    /// Add an import edge with full metadata.
1050    ///
1051    /// Use this method when the import has an alias or is a wildcard import.
1052    /// For simple imports without alias or wildcard, use [`add_import_edge`](Self::add_import_edge).
1053    ///
1054    /// # Arguments
1055    ///
1056    /// * `importer` - The node importing (e.g., module or file)
1057    /// * `imported` - The node being imported
1058    /// * `alias` - Optional alias string (e.g., for `import { foo as bar }`, alias is "bar")
1059    /// * `is_wildcard` - Whether this is a wildcard import (e.g., `import *`)
1060    ///
1061    /// # Canonical Usage
1062    ///
1063    /// | Import Syntax | Method |
1064    /// |---------------|--------|
1065    /// | `import foo` | `add_import_edge(importer, imported)` |
1066    /// | `import foo as bar` | `add_import_edge_full(importer, imported, Some("bar"), false)` |
1067    /// | `import *` / `import *.*` | `add_import_edge_full(importer, imported, None, true)` |
1068    /// | `import * as ns` | `add_import_edge_full(importer, imported, Some("ns"), true)` |
1069    ///
1070    /// # Example
1071    ///
1072    /// ```ignore
1073    /// // import { HashMap as Map } from "std::collections"
1074    /// let alias_id = helper.intern("Map");
1075    /// helper.add_import_edge_full(module_id, hashmap_id, Some("Map"), false);
1076    ///
1077    /// // import * from "lodash"
1078    /// helper.add_import_edge_full(module_id, lodash_id, None, true);
1079    /// ```
1080    pub fn add_import_edge_full(
1081        &mut self,
1082        importer: NodeId,
1083        imported: NodeId,
1084        alias: Option<&str>,
1085        is_wildcard: bool,
1086    ) {
1087        let alias_id = alias.map(|s| self.intern(s));
1088        self.staging.add_edge(
1089            importer,
1090            imported,
1091            EdgeKind::Imports {
1092                alias: alias_id,
1093                is_wildcard,
1094            },
1095            self.file_id,
1096        );
1097    }
1098
1099    /// Add an export edge from module to exported symbol.
1100    ///
1101    /// This method uses default metadata (`kind: ExportKind::Direct`, `alias: None`).
1102    /// Use [`add_export_edge_full`](Self::add_export_edge_full) for re-exports,
1103    /// default exports, namespace exports, or exports with aliases.
1104    pub fn add_export_edge(&mut self, module: NodeId, exported: NodeId) {
1105        self.staging.add_edge(
1106            module,
1107            exported,
1108            EdgeKind::Exports {
1109                kind: ExportKind::Direct,
1110                alias: None,
1111            },
1112            self.file_id,
1113        );
1114    }
1115
1116    /// Add an export edge with full metadata.
1117    ///
1118    /// Use this method for re-exports, default exports, namespace exports,
1119    /// or exports with aliases. For simple direct exports without alias,
1120    /// use [`add_export_edge`](Self::add_export_edge).
1121    ///
1122    /// # Arguments
1123    ///
1124    /// * `module` - The module/file node that contains the export
1125    /// * `exported` - The symbol being exported
1126    /// * `kind` - The kind of export:
1127    ///   - `ExportKind::Direct` - Direct export (`export { foo }`)
1128    ///   - `ExportKind::Reexport` - Re-export from another module (`export { foo } from "mod"`)
1129    ///   - `ExportKind::Default` - Default export (`export default foo`)
1130    ///   - `ExportKind::Namespace` - Namespace export (`export * as ns from "mod"`)
1131    /// * `alias` - Optional alias string (e.g., for `export { foo as bar }`, alias is "bar")
1132    ///
1133    /// # Canonical Usage
1134    ///
1135    /// | Export Syntax (JS/TS) | Method |
1136    /// |-----------------------|--------|
1137    /// | `export { name }` | `add_export_edge(module, name)` |
1138    /// | `export default foo` | `add_export_edge_full(module, foo, ExportKind::Default, None)` |
1139    /// | `export { foo as bar }` | `add_export_edge_full(module, foo, ExportKind::Direct, Some("bar"))` |
1140    /// | `export { foo } from "mod"` | `add_export_edge_full(module, foo, ExportKind::Reexport, None)` |
1141    /// | `export { foo as bar } from "mod"` | `add_export_edge_full(module, foo, ExportKind::Reexport, Some("bar"))` |
1142    /// | `export * from "mod"` | `add_export_edge_full(module, mod, ExportKind::Reexport, None)` |
1143    /// | `export * as ns from "mod"` | `add_export_edge_full(module, mod, ExportKind::Namespace, Some("ns"))` |
1144    ///
1145    /// # Example
1146    ///
1147    /// ```ignore
1148    /// // export default MyComponent;
1149    /// helper.add_export_edge_full(module_id, component_id, ExportKind::Default, None);
1150    ///
1151    /// // export { helper as utilHelper };
1152    /// helper.add_export_edge_full(module_id, helper_id, ExportKind::Direct, Some("utilHelper"));
1153    ///
1154    /// // export * as utils from "./utils";
1155    /// helper.add_export_edge_full(module_id, utils_id, ExportKind::Namespace, Some("utils"));
1156    /// ```
1157    pub fn add_export_edge_full(
1158        &mut self,
1159        module: NodeId,
1160        exported: NodeId,
1161        kind: ExportKind,
1162        alias: Option<&str>,
1163    ) {
1164        let alias_id = alias.map(|s| self.intern(s));
1165        self.staging.add_edge(
1166            module,
1167            exported,
1168            EdgeKind::Exports {
1169                kind,
1170                alias: alias_id,
1171            },
1172            self.file_id,
1173        );
1174    }
1175
1176    /// Add a reference edge (variable/field access).
1177    pub fn add_reference_edge(&mut self, from: NodeId, to: NodeId) {
1178        self.staging
1179            .add_edge(from, to, EdgeKind::References, self.file_id);
1180    }
1181
1182    /// Add a defines edge (module defines symbol).
1183    pub fn add_defines_edge(&mut self, parent: NodeId, child: NodeId) {
1184        self.staging
1185            .add_edge(parent, child, EdgeKind::Defines, self.file_id);
1186    }
1187
1188    /// Add a type-of edge (symbol has type).
1189    /// Add a `TypeOf` edge without context metadata (backward compatibility).
1190    ///
1191    /// For new code, prefer `add_typeof_edge_with_context` to provide semantic context.
1192    pub fn add_typeof_edge(&mut self, source: NodeId, target: NodeId) {
1193        self.add_typeof_edge_with_context(source, target, None, None, None);
1194    }
1195
1196    /// Add a `TypeOf` edge with optional context metadata.
1197    ///
1198    /// # Parameters
1199    /// - `source`: The node that has this type (e.g., variable, function, parameter)
1200    /// - `target`: The type node
1201    /// - `context`: Where this type reference appears (Parameter, Return, Field, Variable, etc.)
1202    /// - `index`: Position/index (for parameters, returns, fields)
1203    /// - `name`: Name (for parameters, returns, fields, variables)
1204    ///
1205    /// # Examples
1206    /// ```ignore
1207    /// // Function parameter: func foo(ctx context.Context)
1208    /// helper.add_typeof_edge_with_context(
1209    ///     func_id,
1210    ///     type_id,
1211    ///     Some(TypeOfContext::Parameter),
1212    ///     Some(0),
1213    ///     Some("ctx"),
1214    /// );
1215    ///
1216    /// // Function return: func bar() error
1217    /// helper.add_typeof_edge_with_context(
1218    ///     func_id,
1219    ///     error_type_id,
1220    ///     Some(TypeOfContext::Return),
1221    ///     Some(0),
1222    ///     None,
1223    /// );
1224    ///
1225    /// // Variable: var x int
1226    /// helper.add_typeof_edge_with_context(
1227    ///     var_id,
1228    ///     int_type_id,
1229    ///     Some(TypeOfContext::Variable),
1230    ///     None,
1231    ///     Some("x"),
1232    /// );
1233    /// ```
1234    pub fn add_typeof_edge_with_context(
1235        &mut self,
1236        source: NodeId,
1237        target: NodeId,
1238        context: Option<TypeOfContext>,
1239        index: Option<u16>,
1240        name: Option<&str>,
1241    ) {
1242        let name_id = name.map(|n| self.intern(n));
1243        self.staging.add_edge(
1244            source,
1245            target,
1246            EdgeKind::TypeOf {
1247                context,
1248                index,
1249                name: name_id,
1250            },
1251            self.file_id,
1252        );
1253    }
1254
1255    /// Add an implements edge (class implements interface).
1256    pub fn add_implements_edge(&mut self, implementor: NodeId, interface: NodeId) {
1257        self.staging
1258            .add_edge(implementor, interface, EdgeKind::Implements, self.file_id);
1259    }
1260
1261    /// Add an inherits edge (class extends class).
1262    pub fn add_inherits_edge(&mut self, child: NodeId, parent: NodeId) {
1263        self.staging
1264            .add_edge(child, parent, EdgeKind::Inherits, self.file_id);
1265    }
1266
1267    /// Add a contains edge (parent contains child, e.g., class contains method).
1268    pub fn add_contains_edge(&mut self, parent: NodeId, child: NodeId) {
1269        self.staging
1270            .add_edge(parent, child, EdgeKind::Contains, self.file_id);
1271    }
1272
1273    /// Add a WebAssembly call edge.
1274    ///
1275    /// Used when JavaScript/TypeScript code instantiates or calls WebAssembly modules:
1276    /// - `WebAssembly.instantiate()` / `WebAssembly.instantiateStreaming()`
1277    /// - `new WebAssembly.Module()` / `new WebAssembly.Instance()`
1278    /// - Calling exported WASM functions
1279    pub fn add_webassembly_edge(&mut self, caller: NodeId, wasm_target: NodeId) {
1280        self.staging
1281            .add_edge(caller, wasm_target, EdgeKind::WebAssemblyCall, self.file_id);
1282    }
1283
1284    /// Add an FFI call edge with the specified calling convention.
1285    ///
1286    /// Used for foreign function interface calls:
1287    /// - Node.js native addons (`.node` files)
1288    /// - ctypes/cffi in Python
1289    /// - JNI in Java
1290    /// - P/Invoke in C#
1291    pub fn add_ffi_edge(&mut self, caller: NodeId, ffi_target: NodeId, convention: FfiConvention) {
1292        self.staging.add_edge(
1293            caller,
1294            ffi_target,
1295            EdgeKind::FfiCall { convention },
1296            self.file_id,
1297        );
1298    }
1299
1300    /// Add an HTTP request edge.
1301    ///
1302    /// Use this when detecting HTTP calls like `fetch()` or `axios.get()`.
1303    pub fn add_http_request_edge(
1304        &mut self,
1305        caller: NodeId,
1306        target: NodeId,
1307        method: HttpMethod,
1308        url: Option<&str>,
1309    ) {
1310        let url_id = url.map(|value| self.intern(value));
1311        self.staging.add_edge(
1312            caller,
1313            target,
1314            EdgeKind::HttpRequest {
1315                method,
1316                url: url_id,
1317            },
1318            self.file_id,
1319        );
1320    }
1321
1322    /// Search `CALL_COMPATIBLE_KINDS` for an existing node with the given
1323    /// canonical qualified name, skipping `exclude` (the caller's own kind).
1324    ///
1325    /// Returns the first matching `NodeId` or `None`. The sweep is read-only —
1326    /// no metadata is mutated on cross-kind reuse (Stage 1 declaration metadata
1327    /// is authoritative).
1328    fn reuse_across_call_compatible_kinds(
1329        &self,
1330        canonical: &str,
1331        exclude: NodeKind,
1332    ) -> Option<NodeId> {
1333        for &kind in CALL_COMPATIBLE_KINDS {
1334            if kind == exclude {
1335                continue;
1336            }
1337            if let Some(&id) = self.node_cache.get(&(canonical.to_string(), kind)) {
1338                return Some(id);
1339            }
1340        }
1341        None
1342    }
1343
1344    /// Ensure a callee node exists for call-edge construction, with a
1345    /// **non-optional** call-site span.
1346    ///
1347    /// This is the preferred API for Stage 2 call-edge building. The span is
1348    /// required so that every stub gets at least the caller's line — never 0.
1349    /// The `kind_hint` guides the sweep order and determines the `NodeKind`
1350    /// used if a fresh node must be created.
1351    ///
1352    /// Cross-kind reuse: if a node with the same canonical qualified name
1353    /// already exists as any call-compatible kind, it is returned as-is.
1354    pub fn ensure_callee(
1355        &mut self,
1356        qualified_name: &str,
1357        call_site_span: Span,
1358        kind_hint: CalleeKindHint,
1359    ) -> NodeId {
1360        let canonical = canonicalize_graph_qualified_name(self.language, qualified_name);
1361        let target_kind = kind_hint.to_node_kind();
1362
1363        // First check for exact-kind cache hit (fast path)
1364        if let Some(&id) = self.node_cache.get(&(canonical.clone(), target_kind)) {
1365            return id;
1366        }
1367        // Then sweep all other call-compatible kinds
1368        if let Some(id) = self.reuse_across_call_compatible_kinds(&canonical, target_kind) {
1369            return id;
1370        }
1371        // Create a new node with the call-site span (never None)
1372        self.add_node_internal(
1373            qualified_name,
1374            Some(call_site_span),
1375            target_kind,
1376            &[],
1377            None,
1378            None,
1379        )
1380    }
1381
1382    /// Ensure a function node exists, creating it if needed.
1383    ///
1384    /// Cross-kind reuse: if a node with the same canonical qualified name
1385    /// already exists as any call-compatible kind (Method, Macro, Constant,
1386    /// `LambdaTarget`), the existing node is returned as-is. This prevents
1387    /// duplicate spanless Function nodes from being created during Stage 2
1388    /// call-edge construction, which would cause `get_references` to silently
1389    /// drop callers due to location-based deduplication at `(file, line=0, col=0)`.
1390    ///
1391    /// The Stage 1 declaration node is authoritative for metadata — no attributes
1392    /// are mutated on cross-kind reuse.
1393    pub fn ensure_function(
1394        &mut self,
1395        qualified_name: &str,
1396        span: Option<Span>,
1397        is_async: bool,
1398        is_unsafe: bool,
1399    ) -> NodeId {
1400        let canonical = canonicalize_graph_qualified_name(self.language, qualified_name);
1401        if let Some(id) = self.reuse_across_call_compatible_kinds(&canonical, NodeKind::Function) {
1402            return id;
1403        }
1404        self.add_function(qualified_name, span, is_async, is_unsafe)
1405    }
1406
1407    /// Ensure a method node exists, creating it if needed.
1408    ///
1409    /// Cross-kind reuse: if a node with the same canonical qualified name
1410    /// already exists as any call-compatible kind (Function, Macro, Constant,
1411    /// `LambdaTarget`), the existing node is returned as-is. See
1412    /// [`ensure_function`](Self::ensure_function) for the rationale.
1413    pub fn ensure_method(
1414        &mut self,
1415        qualified_name: &str,
1416        span: Option<Span>,
1417        is_async: bool,
1418        is_static: bool,
1419    ) -> NodeId {
1420        let canonical = canonicalize_graph_qualified_name(self.language, qualified_name);
1421        if let Some(id) = self.reuse_across_call_compatible_kinds(&canonical, NodeKind::Method) {
1422            return id;
1423        }
1424        self.add_method(qualified_name, span, is_async, is_static)
1425    }
1426
1427    /// Get statistics about what's been staged.
1428    #[must_use]
1429    pub fn stats(&self) -> HelperStats {
1430        let staging_stats = self.staging.stats();
1431        HelperStats {
1432            strings_interned: self.string_cache.len(),
1433            nodes_created: self.node_cache.len(),
1434            nodes_staged: staging_stats.nodes_staged,
1435            edges_staged: staging_stats.edges_staged,
1436        }
1437    }
1438}
1439
1440fn semantic_name_for_node_input(original: &str, canonical: &str) -> String {
1441    if original.contains('/') {
1442        return original.to_string();
1443    }
1444
1445    canonical
1446        .rsplit("::")
1447        .next()
1448        .map_or_else(|| original.to_string(), ToString::to_string)
1449}
1450
1451/// Statistics from `GraphBuildHelper` operations.
1452#[derive(Debug, Clone, Default)]
1453pub struct HelperStats {
1454    /// Number of unique strings interned.
1455    pub strings_interned: usize,
1456    /// Number of unique nodes created.
1457    pub nodes_created: usize,
1458    /// Total nodes staged (from `StagingGraph`).
1459    pub nodes_staged: usize,
1460    /// Total edges staged (from `StagingGraph`).
1461    pub edges_staged: usize,
1462}
1463
1464#[cfg(test)]
1465mod tests {
1466    use super::*;
1467    use crate::graph::node::Position;
1468    use crate::graph::unified::build::staging::StagingOp;
1469    use std::path::PathBuf;
1470
1471    #[test]
1472    fn test_helper_add_function() {
1473        let mut staging = StagingGraph::new();
1474        let file = PathBuf::from("test.rs");
1475        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Rust);
1476
1477        let node_id = helper.add_function("main", None, false, false);
1478        assert!(!node_id.is_invalid());
1479        assert_eq!(helper.stats().nodes_created, 1);
1480    }
1481
1482    #[test]
1483    fn test_helper_deduplication() {
1484        let mut staging = StagingGraph::new();
1485        let file = PathBuf::from("test.rs");
1486        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Rust);
1487
1488        let id1 = helper.add_function("main", None, false, false);
1489        let id2 = helper.add_function("main", None, false, false);
1490
1491        assert_eq!(id1, id2, "Same function should return same NodeId");
1492        assert_eq!(
1493            helper.stats().nodes_created,
1494            1,
1495            "Should only create one node"
1496        );
1497    }
1498
1499    #[test]
1500    fn test_helper_string_interning() {
1501        let mut staging = StagingGraph::new();
1502        let file = PathBuf::from("test.rs");
1503        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Rust);
1504
1505        let s1 = helper.intern("hello");
1506        let s2 = helper.intern("world");
1507        let s3 = helper.intern("hello"); // Duplicate
1508
1509        assert_ne!(s1, s2, "Different strings should have different IDs");
1510        assert_eq!(s1, s3, "Same string should return same ID");
1511        assert_eq!(helper.stats().strings_interned, 2);
1512    }
1513
1514    #[test]
1515    fn test_helper_add_call_edge() {
1516        let mut staging = StagingGraph::new();
1517        let file = PathBuf::from("test.rs");
1518        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Rust);
1519
1520        let main_id = helper.add_function("main", None, false, false);
1521        let helper_id = helper.add_function("helper", None, false, false);
1522
1523        helper.add_call_edge(main_id, helper_id);
1524
1525        assert_eq!(helper.stats().edges_staged, 1);
1526        let edge_kind = staging.operations().iter().find_map(|op| {
1527            if let StagingOp::AddEdge { kind, .. } = op {
1528                Some(kind)
1529            } else {
1530                None
1531            }
1532        });
1533        match edge_kind {
1534            Some(EdgeKind::Calls {
1535                argument_count,
1536                is_async,
1537            }) => {
1538                assert_eq!(*argument_count, 255);
1539                assert!(!*is_async);
1540            }
1541            _ => panic!("Expected Calls edge"),
1542        }
1543    }
1544
1545    #[test]
1546    fn test_helper_multiple_node_kinds() {
1547        let mut staging = StagingGraph::new();
1548        let file = PathBuf::from("test.py");
1549        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Python);
1550
1551        let _class_id = helper.add_class("MyClass", None);
1552        let _method_id = helper.add_method("MyClass.my_method", None, false, false);
1553        let _func_id = helper.add_function("standalone_func", None, true, false);
1554
1555        assert_eq!(helper.stats().nodes_created, 3);
1556    }
1557
1558    #[test]
1559    fn test_helper_canonicalizes_language_native_qualified_names() {
1560        let mut staging = StagingGraph::new();
1561        let file = PathBuf::from("test.py");
1562        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Python);
1563
1564        let _method_id = helper.add_method("pkg.module.run", None, false, false);
1565
1566        let add_node_op = staging
1567            .operations()
1568            .iter()
1569            .find(|op| matches!(op, StagingOp::AddNode { .. }))
1570            .expect("Expected AddNode operation");
1571
1572        if let StagingOp::AddNode { entry, .. } = add_node_op {
1573            assert_eq!(staging.resolve_local_string(entry.name), Some("run"));
1574            assert_eq!(
1575                staging.resolve_node_name(entry),
1576                Some("pkg::module::run"),
1577                "expected GraphBuildHelper to canonicalize Python dotted qualified names"
1578            );
1579        }
1580    }
1581
1582    #[test]
1583    fn test_helper_preserves_path_qualified_names() {
1584        let mut staging = StagingGraph::new();
1585        let file = PathBuf::from("test.js");
1586        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::JavaScript);
1587
1588        let _func_id = helper.add_function("frontend/api.js::fetchUsers", None, false, false);
1589
1590        let add_node_op = staging
1591            .operations()
1592            .iter()
1593            .find(|op| matches!(op, StagingOp::AddNode { .. }))
1594            .expect("Expected AddNode operation");
1595
1596        if let StagingOp::AddNode { entry, .. } = add_node_op {
1597            assert_eq!(
1598                staging.resolve_local_string(entry.name),
1599                Some("frontend/api.js::fetchUsers")
1600            );
1601            assert_eq!(
1602                staging.resolve_node_name(entry),
1603                Some("frontend/api.js::fetchUsers"),
1604                "expected path-qualified names to remain unchanged"
1605            );
1606        }
1607    }
1608
1609    #[test]
1610    fn test_helper_verbatim_import_preserves_resource_name() {
1611        let mut staging = StagingGraph::new();
1612        let file = PathBuf::from("index.html");
1613        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Html);
1614
1615        let _import_id = helper.add_verbatim_import("styles.css", None);
1616
1617        let add_node_op = staging
1618            .operations()
1619            .iter()
1620            .find(|op| matches!(op, StagingOp::AddNode { .. }))
1621            .expect("Expected AddNode operation");
1622
1623        if let StagingOp::AddNode { entry, .. } = add_node_op {
1624            assert_eq!(staging.resolve_local_string(entry.name), Some("styles.css"));
1625            assert_eq!(entry.qualified_name, None);
1626            assert_eq!(
1627                staging.resolve_node_name(entry),
1628                Some("styles.css"),
1629                "expected verbatim resource imports to preserve their literal identity"
1630            );
1631        }
1632    }
1633
1634    #[test]
1635    fn test_helper_verbatim_variable_preserves_resource_name() {
1636        let mut staging = StagingGraph::new();
1637        let file = PathBuf::from("index.html");
1638        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Html);
1639
1640        let _variable_id = helper.add_verbatim_variable("/assets/logo.icon.png", None);
1641
1642        let add_node_op = staging
1643            .operations()
1644            .iter()
1645            .find(|op| matches!(op, StagingOp::AddNode { .. }))
1646            .expect("Expected AddNode operation");
1647
1648        if let StagingOp::AddNode { entry, .. } = add_node_op {
1649            assert_eq!(
1650                staging.resolve_local_string(entry.name),
1651                Some("/assets/logo.icon.png")
1652            );
1653            assert_eq!(entry.qualified_name, None);
1654            assert_eq!(
1655                staging.resolve_node_name(entry),
1656                Some("/assets/logo.icon.png"),
1657                "expected verbatim resource variables to preserve their literal identity"
1658            );
1659        }
1660    }
1661
1662    #[test]
1663    fn test_helper_ensure_function() {
1664        let mut staging = StagingGraph::new();
1665        let file = PathBuf::from("test.rs");
1666        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Rust);
1667
1668        let id1 = helper.ensure_function("foo", None, false, false);
1669        let id2 = helper.ensure_function("foo", None, true, false); // Different attrs, same name
1670
1671        assert_eq!(id1, id2, "ensure_function should be idempotent by name");
1672    }
1673
1674    #[test]
1675    fn test_helper_with_span() {
1676        let mut staging = StagingGraph::new();
1677        let file = PathBuf::from("test.rs");
1678        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Rust);
1679
1680        let span = Span {
1681            start: Position {
1682                line: 10,
1683                column: 0,
1684            },
1685            end: Position {
1686                line: 15,
1687                column: 1,
1688            },
1689        };
1690
1691        let node_id = helper.add_function("main", Some(span), false, false);
1692        assert!(!node_id.is_invalid());
1693    }
1694
1695    #[test]
1696    fn test_helper_add_call_edge_full() {
1697        let mut staging = StagingGraph::new();
1698        let file = PathBuf::from("test.rs");
1699        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Rust);
1700
1701        let caller_id = helper.add_function("caller", None, false, false);
1702        let callee_id = helper.add_function("callee", None, false, false);
1703
1704        // Add a call with specific metadata
1705        helper.add_call_edge_full(caller_id, callee_id, 3, true);
1706
1707        assert_eq!(helper.stats().edges_staged, 1);
1708
1709        // Verify the edge has correct metadata
1710        let edges = staging.operations();
1711        let call_edge = edges.iter().find(|op| {
1712            matches!(
1713                op,
1714                StagingOp::AddEdge {
1715                    kind: EdgeKind::Calls { .. },
1716                    ..
1717                }
1718            )
1719        });
1720
1721        assert!(call_edge.is_some());
1722        if let StagingOp::AddEdge {
1723            kind:
1724                EdgeKind::Calls {
1725                    argument_count,
1726                    is_async,
1727                },
1728            ..
1729        } = call_edge.unwrap()
1730        {
1731            assert_eq!(*argument_count, 3);
1732            assert!(*is_async);
1733        }
1734    }
1735
1736    #[test]
1737    fn test_helper_add_import_edge_full() {
1738        let mut staging = StagingGraph::new();
1739        let file = PathBuf::from("test.js");
1740        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::JavaScript);
1741
1742        let module_id = helper.add_module("app", None);
1743        let imported_id = helper.add_function("utils", None, false, false);
1744
1745        // Import with alias
1746        helper.add_import_edge_full(module_id, imported_id, Some("helpers"), false);
1747
1748        assert_eq!(helper.stats().edges_staged, 1);
1749
1750        // Verify the edge has correct metadata
1751        let edges = staging.operations();
1752        let import_edge = edges.iter().find(|op| {
1753            matches!(
1754                op,
1755                StagingOp::AddEdge {
1756                    kind: EdgeKind::Imports { .. },
1757                    ..
1758                }
1759            )
1760        });
1761
1762        assert!(import_edge.is_some());
1763        if let StagingOp::AddEdge {
1764            kind: EdgeKind::Imports { alias, is_wildcard },
1765            ..
1766        } = import_edge.unwrap()
1767        {
1768            assert!(alias.is_some(), "Alias should be present");
1769            assert!(!*is_wildcard);
1770        }
1771    }
1772
1773    #[test]
1774    fn test_helper_add_import_edge_wildcard() {
1775        let mut staging = StagingGraph::new();
1776        let file = PathBuf::from("test.js");
1777        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::JavaScript);
1778
1779        let module_id = helper.add_module("app", None);
1780        let imported_id = helper.add_module("lodash", None);
1781
1782        // Wildcard import: import * from "lodash"
1783        helper.add_import_edge_full(module_id, imported_id, None, true);
1784
1785        let edges = staging.operations();
1786        let import_edge = edges.iter().find(|op| {
1787            matches!(
1788                op,
1789                StagingOp::AddEdge {
1790                    kind: EdgeKind::Imports { .. },
1791                    ..
1792                }
1793            )
1794        });
1795
1796        if let StagingOp::AddEdge {
1797            kind: EdgeKind::Imports { alias, is_wildcard },
1798            ..
1799        } = import_edge.unwrap()
1800        {
1801            assert!(alias.is_none());
1802            assert!(*is_wildcard);
1803        }
1804    }
1805
1806    #[test]
1807    fn test_helper_add_export_edge_full() {
1808        let mut staging = StagingGraph::new();
1809        let file = PathBuf::from("test.js");
1810        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::JavaScript);
1811
1812        let module_id = helper.add_module("app", None);
1813        let component_id = helper.add_class("MyComponent", None);
1814
1815        // Default export
1816        helper.add_export_edge_full(module_id, component_id, ExportKind::Default, None);
1817
1818        assert_eq!(helper.stats().edges_staged, 1);
1819
1820        let edges = staging.operations();
1821        let export_edge = edges.iter().find(|op| {
1822            matches!(
1823                op,
1824                StagingOp::AddEdge {
1825                    kind: EdgeKind::Exports { .. },
1826                    ..
1827                }
1828            )
1829        });
1830
1831        assert!(export_edge.is_some());
1832        if let StagingOp::AddEdge {
1833            kind: EdgeKind::Exports { kind, alias },
1834            ..
1835        } = export_edge.unwrap()
1836        {
1837            assert_eq!(*kind, ExportKind::Default);
1838            assert!(alias.is_none());
1839        }
1840    }
1841
1842    #[test]
1843    fn test_helper_add_export_edge_with_alias() {
1844        let mut staging = StagingGraph::new();
1845        let file = PathBuf::from("test.js");
1846        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::JavaScript);
1847
1848        let module_id = helper.add_module("app", None);
1849        let helper_fn_id = helper.add_function("internalHelper", None, false, false);
1850
1851        // export { internalHelper as helper }
1852        helper.add_export_edge_full(module_id, helper_fn_id, ExportKind::Direct, Some("helper"));
1853
1854        let edges = staging.operations();
1855        let export_edge = edges.iter().find(|op| {
1856            matches!(
1857                op,
1858                StagingOp::AddEdge {
1859                    kind: EdgeKind::Exports { .. },
1860                    ..
1861                }
1862            )
1863        });
1864
1865        if let StagingOp::AddEdge {
1866            kind: EdgeKind::Exports { kind, alias },
1867            ..
1868        } = export_edge.unwrap()
1869        {
1870            assert_eq!(*kind, ExportKind::Direct);
1871            assert!(alias.is_some(), "Alias should be present");
1872        }
1873    }
1874
1875    #[test]
1876    fn test_helper_add_export_edge_reexport() {
1877        let mut staging = StagingGraph::new();
1878        let file = PathBuf::from("index.js");
1879        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::JavaScript);
1880
1881        let module_id = helper.add_module("index", None);
1882        let utils_id = helper.add_module("utils", None);
1883
1884        // export * as utils from "./utils"
1885        helper.add_export_edge_full(module_id, utils_id, ExportKind::Namespace, Some("utils"));
1886
1887        let edges = staging.operations();
1888        let export_edge = edges.iter().find(|op| {
1889            matches!(
1890                op,
1891                StagingOp::AddEdge {
1892                    kind: EdgeKind::Exports { .. },
1893                    ..
1894                }
1895            )
1896        });
1897
1898        if let StagingOp::AddEdge {
1899            kind: EdgeKind::Exports { kind, alias },
1900            ..
1901        } = export_edge.unwrap()
1902        {
1903            assert_eq!(*kind, ExportKind::Namespace);
1904            assert!(alias.is_some());
1905        }
1906    }
1907
1908    #[test]
1909    fn test_helper_add_call_edge_full_with_span() {
1910        let mut staging = StagingGraph::new();
1911        let file = PathBuf::from("test.rs");
1912        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Rust);
1913
1914        let caller_id = helper.add_function("caller", None, false, false);
1915        let callee_id = helper.add_function("callee", None, false, false);
1916
1917        let span = Span {
1918            start: Position { line: 5, column: 4 },
1919            end: Position {
1920                line: 5,
1921                column: 20,
1922            },
1923        };
1924
1925        helper.add_call_edge_full_with_span(caller_id, callee_id, 2, false, vec![span]);
1926
1927        let edges = staging.operations();
1928        let call_edge = edges.iter().find(|op| {
1929            matches!(
1930                op,
1931                StagingOp::AddEdge {
1932                    kind: EdgeKind::Calls { .. },
1933                    ..
1934                }
1935            )
1936        });
1937
1938        if let StagingOp::AddEdge {
1939            kind:
1940                EdgeKind::Calls {
1941                    argument_count,
1942                    is_async,
1943                },
1944            spans: edge_spans,
1945            ..
1946        } = call_edge.unwrap()
1947        {
1948            assert_eq!(*argument_count, 2);
1949            assert!(!*is_async);
1950            assert!(!edge_spans.is_empty());
1951        }
1952    }
1953
1954    #[test]
1955    fn test_helper_add_function_with_async_attribute() {
1956        let mut staging = StagingGraph::new();
1957        let file = PathBuf::from("test.kt");
1958        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Kotlin);
1959
1960        // Add an async (suspend) function
1961        let _func_id = helper.add_function("fetchData", None, true, false);
1962
1963        // Verify the staged node has is_async = true
1964        let ops = staging.operations();
1965        let add_node_op = ops
1966            .iter()
1967            .find(|op| matches!(op, StagingOp::AddNode { .. }));
1968
1969        assert!(add_node_op.is_some(), "Expected AddNode operation");
1970        if let StagingOp::AddNode { entry, .. } = add_node_op.unwrap() {
1971            assert!(
1972                entry.is_async,
1973                "Expected is_async=true for suspend function, got is_async=false"
1974            );
1975        }
1976    }
1977
1978    #[test]
1979    fn test_helper_add_method_with_static_attribute() {
1980        let mut staging = StagingGraph::new();
1981        let file = PathBuf::from("test.java");
1982        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Java);
1983
1984        // Add a static method
1985        let _method_id = helper.add_method("MyClass.staticMethod", None, false, true);
1986
1987        // Verify the staged node has is_static = true
1988        let ops = staging.operations();
1989        let add_node_op = ops
1990            .iter()
1991            .find(|op| matches!(op, StagingOp::AddNode { .. }));
1992
1993        assert!(add_node_op.is_some(), "Expected AddNode operation");
1994        if let StagingOp::AddNode { entry, .. } = add_node_op.unwrap() {
1995            assert!(
1996                entry.is_static,
1997                "Expected is_static=true for static method, got is_static=false"
1998            );
1999        }
2000    }
2001
2002    #[test]
2003    fn test_helper_add_function_without_attributes() {
2004        let mut staging = StagingGraph::new();
2005        let file = PathBuf::from("test.rs");
2006        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Rust);
2007
2008        // Add a regular function (not async, not unsafe)
2009        let _func_id = helper.add_function("regular_function", None, false, false);
2010
2011        // Verify the staged node has is_async = false
2012        let ops = staging.operations();
2013        let add_node_op = ops
2014            .iter()
2015            .find(|op| matches!(op, StagingOp::AddNode { .. }));
2016
2017        assert!(add_node_op.is_some(), "Expected AddNode operation");
2018        if let StagingOp::AddNode { entry, .. } = add_node_op.unwrap() {
2019            assert!(
2020                !entry.is_async,
2021                "Expected is_async=false for regular function"
2022            );
2023            assert!(
2024                !entry.is_static,
2025                "Expected is_static=false for regular function"
2026            );
2027        }
2028    }
2029
2030    #[test]
2031    fn test_helper_add_method_with_both_attributes() {
2032        let mut staging = StagingGraph::new();
2033        let file = PathBuf::from("test.kt");
2034        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Kotlin);
2035
2036        // Add an async static method
2037        let _method_id = helper.add_method("Service.asyncStaticMethod", None, true, true);
2038
2039        // Verify the staged node has both flags set
2040        let ops = staging.operations();
2041        let add_node_op = ops
2042            .iter()
2043            .find(|op| matches!(op, StagingOp::AddNode { .. }));
2044
2045        assert!(add_node_op.is_some(), "Expected AddNode operation");
2046        if let StagingOp::AddNode { entry, .. } = add_node_op.unwrap() {
2047            assert!(entry.is_async, "Expected is_async=true for async method");
2048            assert!(entry.is_static, "Expected is_static=true for static method");
2049        }
2050    }
2051
2052    #[test]
2053    fn test_helper_add_function_with_unsafe_attribute() {
2054        let mut staging = StagingGraph::new();
2055        let file = PathBuf::from("test.rs");
2056        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Rust);
2057
2058        // Add an unsafe function
2059        let _func_id = helper.add_function("unsafe_function", None, false, true);
2060
2061        // Verify the staged node has is_unsafe = true
2062        let ops = staging.operations();
2063        let add_node_op = ops
2064            .iter()
2065            .find(|op| matches!(op, StagingOp::AddNode { .. }));
2066
2067        assert!(add_node_op.is_some(), "Expected AddNode operation");
2068        if let StagingOp::AddNode { entry, .. } = add_node_op.unwrap() {
2069            assert!(
2070                entry.is_unsafe,
2071                "Expected is_unsafe=true for unsafe function, got is_unsafe={}",
2072                entry.is_unsafe
2073            );
2074        }
2075    }
2076
2077    // ========================================================================
2078    // Cross-kind reuse tests (Method/Function NodeKind mismatch fix)
2079    // ========================================================================
2080
2081    #[test]
2082    fn test_ensure_function_reuses_existing_method_node() {
2083        let mut staging = StagingGraph::new();
2084        let file = PathBuf::from("test.ts");
2085        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::TypeScript);
2086
2087        let span = Span::new(
2088            Position { line: 5, column: 4 },
2089            Position {
2090                line: 10,
2091                column: 5,
2092            },
2093        );
2094
2095        // Stage 1: create a Method node with proper span
2096        let method_id = helper.add_method("MyClass.doWork", Some(span), true, false);
2097
2098        // Stage 2: ensure_function for the same qualified name
2099        let reused_id = helper.ensure_function("MyClass.doWork", None, true, false);
2100
2101        assert_eq!(
2102            method_id, reused_id,
2103            "ensure_function should reuse the existing Method node"
2104        );
2105        assert_eq!(
2106            helper.stats().nodes_created,
2107            1,
2108            "Only the Method node should exist"
2109        );
2110    }
2111
2112    #[test]
2113    fn test_ensure_method_reuses_existing_function_node() {
2114        let mut staging = StagingGraph::new();
2115        let file = PathBuf::from("test.ts");
2116        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::TypeScript);
2117
2118        let func_id = helper.add_function("standalone", None, false, false);
2119        let reused_id = helper.ensure_method("standalone", None, false, false);
2120
2121        assert_eq!(
2122            func_id, reused_id,
2123            "ensure_method should reuse the existing function node"
2124        );
2125        assert_eq!(helper.stats().nodes_created, 1);
2126    }
2127
2128    #[test]
2129    fn test_ensure_function_creates_new_when_no_method_exists() {
2130        let mut staging = StagingGraph::new();
2131        let file = PathBuf::from("test.ts");
2132        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::TypeScript);
2133
2134        let func_id = helper.ensure_function("topLevel", None, false, false);
2135        assert!(!func_id.is_invalid());
2136        assert_eq!(helper.stats().nodes_created, 1);
2137
2138        let func_id2 = helper.ensure_function("topLevel", None, false, false);
2139        assert_eq!(func_id, func_id2);
2140        assert_eq!(helper.stats().nodes_created, 1);
2141    }
2142
2143    #[test]
2144    fn test_no_method_function_duplicate_after_cross_kind_reuse() {
2145        let mut staging = StagingGraph::new();
2146        let file = PathBuf::from("browser-manager.ts");
2147        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::TypeScript);
2148
2149        let span_a = Span::new(
2150            Position { line: 3, column: 4 },
2151            Position { line: 8, column: 5 },
2152        );
2153        let span_b = Span::new(
2154            Position {
2155                line: 10,
2156                column: 4,
2157            },
2158            Position {
2159                line: 15,
2160                column: 5,
2161            },
2162        );
2163
2164        // Stage 1: create Method nodes
2165        let _method_a = helper.add_method("BrowserManager.newTab", Some(span_a), true, false);
2166        let _method_b = helper.add_method("BrowserManager.restoreState", Some(span_b), true, false);
2167
2168        // Stage 2: ensure_function for call-edge construction
2169        let _caller_a = helper.ensure_function("BrowserManager.newTab", None, true, false);
2170        let _caller_b = helper.ensure_function("BrowserManager.restoreState", None, true, false);
2171
2172        // Verify: no same-name Method/NodeKind::Function duplicates
2173        let ops = staging.operations();
2174        let mut method_names = std::collections::HashSet::new();
2175        let mut function_names = std::collections::HashSet::new();
2176
2177        for op in ops {
2178            if let StagingOp::AddNode { entry, .. } = op {
2179                if entry.kind == NodeKind::Method {
2180                    method_names.insert(entry.name);
2181                } else if entry.kind == NodeKind::Function {
2182                    function_names.insert(entry.name);
2183                }
2184            }
2185        }
2186
2187        let overlap: Vec<_> = method_names.intersection(&function_names).collect();
2188        assert!(
2189            overlap.is_empty(),
2190            "Found names that are both Method and Function: {overlap:?}"
2191        );
2192    }
2193
2194    // ========================================================================
2195    // Generalized cross-kind reuse tests (HU01: CALL_COMPATIBLE_KINDS)
2196    // ========================================================================
2197
2198    #[test]
2199    fn test_ensure_function_reuses_existing_macro_node() {
2200        let mut staging = StagingGraph::new();
2201        let file = PathBuf::from("test.c");
2202        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::C);
2203
2204        let span = Span::new(
2205            Position { line: 1, column: 0 },
2206            Position {
2207                line: 1,
2208                column: 40,
2209            },
2210        );
2211
2212        // Stage 1: create a Macro node (e.g., list_for_each_entry in C kernel code)
2213        let macro_id = helper.add_node("list_for_each_entry", Some(span), NodeKind::Macro);
2214
2215        // Stage 2: ensure_function for the same name (call-edge construction)
2216        let reused_id = helper.ensure_function("list_for_each_entry", None, false, false);
2217
2218        assert_eq!(
2219            macro_id, reused_id,
2220            "ensure_function should reuse the existing Macro node"
2221        );
2222        assert_eq!(helper.stats().nodes_created, 1);
2223    }
2224
2225    #[test]
2226    fn test_ensure_function_reuses_existing_constant_node() {
2227        let mut staging = StagingGraph::new();
2228        let file = PathBuf::from("test.c");
2229        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::C);
2230
2231        let span = Span::new(
2232            Position { line: 3, column: 0 },
2233            Position {
2234                line: 3,
2235                column: 30,
2236            },
2237        );
2238
2239        // A function pointer constant in C
2240        let const_id = helper.add_constant("handler_fn", Some(span));
2241
2242        let reused_id = helper.ensure_function("handler_fn", None, false, false);
2243
2244        assert_eq!(
2245            const_id, reused_id,
2246            "ensure_function should reuse the existing Constant node"
2247        );
2248        assert_eq!(helper.stats().nodes_created, 1);
2249    }
2250
2251    #[test]
2252    fn test_ensure_method_reuses_existing_lambda_target_node() {
2253        let mut staging = StagingGraph::new();
2254        let file = PathBuf::from("test.java");
2255        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Java);
2256
2257        let span = Span::new(
2258            Position { line: 7, column: 8 },
2259            Position {
2260                line: 10,
2261                column: 9,
2262            },
2263        );
2264
2265        let lambda_id = helper.add_node("Comparator.compare", Some(span), NodeKind::LambdaTarget);
2266
2267        let reused_id = helper.ensure_method("Comparator.compare", None, false, false);
2268
2269        assert_eq!(
2270            lambda_id, reused_id,
2271            "ensure_method should reuse the existing LambdaTarget node"
2272        );
2273        assert_eq!(helper.stats().nodes_created, 1);
2274    }
2275
2276    #[test]
2277    fn test_cross_kind_reuse_does_not_merge_incompatible_kinds() {
2278        let mut staging = StagingGraph::new();
2279        let file = PathBuf::from("test.css");
2280        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Css);
2281
2282        // Create a StyleRule node — NOT a call-compatible kind
2283        let style_id =
2284            helper.add_node_verbatim(".container", None, NodeKind::StyleRule, &[], None, None);
2285
2286        // ensure_function with the same name should NOT reuse the StyleRule
2287        let func_id = helper.ensure_function(".container", None, false, false);
2288
2289        assert_ne!(
2290            style_id, func_id,
2291            "ensure_function must NOT merge into a StyleRule"
2292        );
2293        assert_eq!(helper.stats().nodes_created, 2);
2294    }
2295
2296    // ========================================================================
2297    // Stub-first order tests (Codex review M1: ensure_* before add_*)
2298    // Proves cross-kind reuse works when the STUB is created first and
2299    // the real declaration arrives later — the actual line-zero failure mode.
2300    // ========================================================================
2301
2302    #[test]
2303    fn test_stub_first_ensure_function_then_add_method_reuses() {
2304        let mut staging = StagingGraph::new();
2305        let file = PathBuf::from("test.ts");
2306        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::TypeScript);
2307
2308        // Stage 2 runs first (call-edge construction creates a Function stub)
2309        let stub_id = helper.ensure_function("Widget.render", None, false, false);
2310
2311        // Stage 1 runs later (declaration extraction creates Method with real span)
2312        let span = Span::new(
2313            Position {
2314                line: 10,
2315                column: 4,
2316            },
2317            Position {
2318                line: 20,
2319                column: 5,
2320            },
2321        );
2322        let decl_id = helper.add_method("Widget.render", Some(span), false, false);
2323
2324        // The two calls should produce DIFFERENT NodeIds because add_method
2325        // uses its own (name, Method) cache key while ensure_function created
2326        // (name, Function). This is the scenario Phase 4c-prime unifies later.
2327        // What matters here: NO PANIC, and both IDs are valid.
2328        assert!(!stub_id.is_invalid());
2329        assert!(!decl_id.is_invalid());
2330        // If they are different, Phase 4c-prime handles the merge.
2331        // If add_node_internal deduped them (same canonical), that's also fine.
2332    }
2333
2334    #[test]
2335    fn test_stub_first_ensure_method_then_add_function_reuses() {
2336        let mut staging = StagingGraph::new();
2337        let file = PathBuf::from("test.py");
2338        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Python);
2339
2340        // Stub created first
2341        let stub_id = helper.ensure_method("process_data", None, false, false);
2342
2343        // Real declaration arrives
2344        let span = Span::new(
2345            Position { line: 5, column: 0 },
2346            Position {
2347                line: 15,
2348                column: 0,
2349            },
2350        );
2351        let decl_id = helper.add_function("process_data", Some(span), false, false);
2352
2353        assert!(!stub_id.is_invalid());
2354        assert!(!decl_id.is_invalid());
2355    }
2356
2357    #[test]
2358    fn test_ensure_callee_then_add_function_same_name_no_panic() {
2359        let mut staging = StagingGraph::new();
2360        let file = PathBuf::from("test.c");
2361        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::C);
2362
2363        let call_span = Span::new(
2364            Position {
2365                line: 50,
2366                column: 4,
2367            },
2368            Position {
2369                line: 50,
2370                column: 20,
2371            },
2372        );
2373        let callee_id = helper.ensure_callee("kfree", call_span, CalleeKindHint::Function);
2374
2375        let def_span = Span::new(
2376            Position { line: 1, column: 0 },
2377            Position {
2378                line: 10,
2379                column: 1,
2380            },
2381        );
2382        let def_id = helper.add_function("kfree", Some(def_span), false, false);
2383
2384        // ensure_callee already created a Function node for "kfree", so
2385        // add_function should return the same NodeId (same cache key).
2386        assert_eq!(
2387            callee_id, def_id,
2388            "add_function should reuse the node created by ensure_callee"
2389        );
2390        assert_eq!(helper.stats().nodes_created, 1);
2391    }
2392
2393    // ========================================================================
2394    // ensure_callee tests (HU02)
2395    // ========================================================================
2396
2397    #[test]
2398    fn test_ensure_callee_function_hint_creates_with_span() {
2399        let mut staging = StagingGraph::new();
2400        let file = PathBuf::from("test.rs");
2401        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Rust);
2402
2403        let call_span = Span::new(
2404            Position {
2405                line: 20,
2406                column: 4,
2407            },
2408            Position {
2409                line: 20,
2410                column: 30,
2411            },
2412        );
2413
2414        let id = helper.ensure_callee("target_fn", call_span, CalleeKindHint::Function);
2415        assert!(!id.is_invalid());
2416
2417        // The created node should have start_line > 0 (from the call-site span)
2418        let ops = staging.operations();
2419        let node_op = ops
2420            .iter()
2421            .find(|op| matches!(op, StagingOp::AddNode { .. }));
2422        if let Some(StagingOp::AddNode { entry, .. }) = node_op {
2423            assert!(
2424                entry.start_line > 0,
2425                "ensure_callee must produce nodes with line > 0"
2426            );
2427        }
2428    }
2429
2430    #[test]
2431    fn test_ensure_callee_macro_hint_reuses_existing_macro() {
2432        let mut staging = StagingGraph::new();
2433        let file = PathBuf::from("test.c");
2434        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::C);
2435
2436        let def_span = Span::new(
2437            Position { line: 5, column: 0 },
2438            Position {
2439                line: 5,
2440                column: 40,
2441            },
2442        );
2443        let call_span = Span::new(
2444            Position {
2445                line: 99,
2446                column: 4,
2447            },
2448            Position {
2449                line: 99,
2450                column: 30,
2451            },
2452        );
2453
2454        let macro_id = helper.add_node("IS_ERR", Some(def_span), NodeKind::Macro);
2455        let reused_id = helper.ensure_callee("IS_ERR", call_span, CalleeKindHint::Macro);
2456
2457        assert_eq!(
2458            macro_id, reused_id,
2459            "ensure_callee should reuse existing Macro node"
2460        );
2461        assert_eq!(helper.stats().nodes_created, 1);
2462    }
2463
2464    #[test]
2465    fn test_ensure_callee_idempotent_returns_first_spans_node() {
2466        let mut staging = StagingGraph::new();
2467        let file = PathBuf::from("test.rs");
2468        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Rust);
2469
2470        let span1 = Span::new(
2471            Position {
2472                line: 10,
2473                column: 0,
2474            },
2475            Position {
2476                line: 10,
2477                column: 20,
2478            },
2479        );
2480        let span2 = Span::new(
2481            Position {
2482                line: 50,
2483                column: 0,
2484            },
2485            Position {
2486                line: 50,
2487                column: 20,
2488            },
2489        );
2490
2491        let id1 = helper.ensure_callee("func", span1, CalleeKindHint::Function);
2492        let id2 = helper.ensure_callee("func", span2, CalleeKindHint::Function);
2493
2494        assert_eq!(
2495            id1, id2,
2496            "Two ensure_callee calls for the same name return the same NodeId"
2497        );
2498    }
2499
2500    #[test]
2501    fn test_call_compatible_kinds_dry_no_body_changes_needed() {
2502        // Compile-time proof: adding a variant to CALL_COMPATIBLE_KINDS does
2503        // NOT require touching ensure_function or ensure_method bodies. Both
2504        // delegate to reuse_across_call_compatible_kinds which iterates the
2505        // const slice. This test simply asserts the slice contains the expected
2506        // entries to catch accidental removals.
2507        assert!(CALL_COMPATIBLE_KINDS.contains(&NodeKind::Function));
2508        assert!(CALL_COMPATIBLE_KINDS.contains(&NodeKind::Method));
2509        assert!(CALL_COMPATIBLE_KINDS.contains(&NodeKind::Macro));
2510        assert!(CALL_COMPATIBLE_KINDS.contains(&NodeKind::Constant));
2511        assert!(CALL_COMPATIBLE_KINDS.contains(&NodeKind::LambdaTarget));
2512        assert_eq!(CALL_COMPATIBLE_KINDS.len(), 5);
2513    }
2514}