Skip to main content

sqry_core/graph/
node.rs

1//! Node types for the unified code graph
2//!
3//! This module defines the core node types that represent code entities
4//! (functions, classes, modules, etc.) in the unified graph architecture.
5
6use serde::{Deserialize, Serialize};
7use std::fmt;
8use std::sync::Arc;
9
10/// Language identifier
11#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
12pub enum Language {
13    /// C language
14    C,
15    /// C++ language
16    Cpp,
17    /// C# language
18    CSharp,
19    /// CSS language
20    Css,
21    /// JavaScript language
22    JavaScript,
23    /// Python language
24    Python,
25    /// TypeScript language
26    TypeScript,
27    /// Rust language
28    Rust,
29    /// Go language
30    Go,
31    /// Java language
32    Java,
33    /// Ruby language
34    Ruby,
35    /// PHP language
36    Php,
37    /// Swift language
38    Swift,
39    /// Kotlin language
40    Kotlin,
41    /// Scala language
42    Scala,
43    /// SQL language
44    Sql,
45    /// Dart language
46    Dart,
47    /// Lua language
48    Lua,
49    /// Perl language
50    Perl,
51    /// Shell (Bash) language
52    Shell,
53    /// Groovy language
54    Groovy,
55    /// Elixir language
56    Elixir,
57    /// R language
58    R,
59    /// Haskell language
60    Haskell,
61    /// HTML language
62    Html,
63    /// Svelte language
64    Svelte,
65    /// Vue language
66    Vue,
67    /// Zig language
68    Zig,
69    /// Terraform (HCL) language
70    Terraform,
71    /// Puppet language
72    Puppet,
73    /// Pulumi language
74    Pulumi,
75    /// Virtual language for HTTP endpoints
76    Http,
77    /// Oracle PL/SQL language
78    Plsql,
79    /// Salesforce Apex language
80    Apex,
81    /// SAP ABAP language
82    Abap,
83    /// `ServiceNow` (Xanadu) language
84    ServiceNow,
85}
86
87impl fmt::Display for Language {
88    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
89        match self {
90            Language::C => write!(f, "c"),
91            Language::Cpp => write!(f, "cpp"),
92            Language::CSharp => write!(f, "csharp"),
93            Language::Css => write!(f, "css"),
94            Language::JavaScript => write!(f, "js"),
95            Language::Python => write!(f, "py"),
96            Language::TypeScript => write!(f, "ts"),
97            Language::Rust => write!(f, "rust"),
98            Language::Go => write!(f, "go"),
99            Language::Java => write!(f, "java"),
100            Language::Ruby => write!(f, "ruby"),
101            Language::Php => write!(f, "php"),
102            Language::Swift => write!(f, "swift"),
103            Language::Kotlin => write!(f, "kotlin"),
104            Language::Scala => write!(f, "scala"),
105            Language::Sql => write!(f, "sql"),
106            Language::Dart => write!(f, "dart"),
107            Language::Lua => write!(f, "lua"),
108            Language::Perl => write!(f, "perl"),
109            Language::Shell => write!(f, "shell"),
110            Language::Groovy => write!(f, "groovy"),
111            Language::Elixir => write!(f, "elixir"),
112            Language::R => write!(f, "r"),
113            Language::Haskell => write!(f, "haskell"),
114            Language::Html => write!(f, "html"),
115            Language::Svelte => write!(f, "svelte"),
116            Language::Vue => write!(f, "vue"),
117            Language::Zig => write!(f, "zig"),
118            Language::Terraform => write!(f, "terraform"),
119            Language::Puppet => write!(f, "puppet"),
120            Language::Pulumi => write!(f, "pulumi"),
121            Language::Http => write!(f, "http"),
122            Language::Plsql => write!(f, "plsql"),
123            Language::Apex => write!(f, "apex"),
124            Language::Abap => write!(f, "abap"),
125            Language::ServiceNow => write!(f, "servicenow"),
126        }
127    }
128}
129
130/// Universal node identifier with string interning for memory efficiency
131///
132/// Per AGENTS.md:149-151, uses `Arc<str>` to reduce memory usage for
133/// symbol-heavy data structures (saves 10-50 MB for typical repos).
134///
135/// # Examples
136///
137/// ```
138/// use sqry_core::graph::node::{NodeId, Language};
139/// use std::sync::Arc;
140///
141/// let node_id = NodeId::new(
142///     Language::Cpp,
143///     "src/main.cpp",
144///     "main"
145/// );
146///
147/// // Arc<str> makes cloning cheap (only refcount increment)
148/// let cloned = node_id.clone();
149/// assert_eq!(node_id, cloned);
150/// ```
151#[derive(Debug, Clone, Hash, Eq, PartialEq, Ord, PartialOrd)]
152pub struct NodeId {
153    /// Language of origin
154    pub language: Language,
155    /// File path (interned via `Arc<str>`)
156    pub file: Arc<str>,
157    /// Qualified name (interned via `Arc<str>`)
158    /// Examples: "`std::vector::push_back`", "MyClass.process", "__main__"
159    pub qualified_name: Arc<str>,
160}
161
162impl NodeId {
163    /// Create a new `NodeId` with string interning
164    ///
165    /// Automatically interns strings via `Arc<str>` for memory efficiency.
166    ///
167    /// # Examples
168    ///
169    /// ```
170    /// use sqry_core::graph::node::{NodeId, Language};
171    ///
172    /// let id = NodeId::new(Language::Python, "api.py", "User.authenticate");
173    /// println!("{}", id); // "py:api.py:User.authenticate"
174    /// ```
175    pub fn new(language: Language, file: impl AsRef<str>, qualified_name: impl AsRef<str>) -> Self {
176        Self {
177            language,
178            file: Arc::from(file.as_ref()),
179            qualified_name: Arc::from(qualified_name.as_ref()),
180        }
181    }
182
183    /// Get the symbol name without namespace qualification
184    ///
185    /// # Examples
186    ///
187    /// ```
188    /// use sqry_core::graph::node::{NodeId, Language};
189    ///
190    /// let id = NodeId::new(Language::Cpp, "main.cpp", "std::vector::push_back");
191    /// assert_eq!(id.symbol_name(), "push_back");
192    /// ```
193    #[must_use]
194    pub fn symbol_name(&self) -> &str {
195        // Try C++ style first (::), then Python/Java style (.)
196        if let Some(name) = self.qualified_name.rsplit("::").next()
197            && name != self.qualified_name.as_ref()
198        {
199            return name;
200        }
201
202        if let Some(name) = self.qualified_name.rsplit('.').next() {
203            return name;
204        }
205
206        &self.qualified_name
207    }
208}
209
210impl fmt::Display for NodeId {
211    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
212        write!(f, "{}:{}:{}", self.language, self.file, self.qualified_name)
213    }
214}
215
216/// Source code span (line and column information)
217#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq, Default, Serialize, Deserialize)]
218pub struct Span {
219    /// Starting position
220    pub start: Position,
221    /// Ending position
222    pub end: Position,
223}
224
225impl Span {
226    /// Create a new span
227    #[must_use]
228    pub fn new(start: Position, end: Position) -> Self {
229        Self { start, end }
230    }
231
232    /// Create a span from byte offsets (legacy compatibility)
233    #[must_use]
234    pub fn from_bytes(start: usize, end: usize) -> Self {
235        Self {
236            start: Position {
237                line: 0,
238                column: start,
239            },
240            end: Position {
241                line: 0,
242                column: end,
243            },
244        }
245    }
246}
247
248/// Position in source code (line and column)
249#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq, Default, Serialize, Deserialize)]
250pub struct Position {
251    /// Line number (0-indexed)
252    pub line: usize,
253    /// Column number (0-indexed)
254    pub column: usize,
255}
256
257impl Position {
258    /// Create a new position
259    #[must_use]
260    pub fn new(line: usize, column: usize) -> Self {
261        Self { line, column }
262    }
263}
264
265/// Type of code entity
266#[derive(Debug, Clone, PartialEq)]
267pub enum NodeKind {
268    /// Function or method
269    Function {
270        /// Function parameters
271        params: Vec<Param>,
272        /// Return type (if known)
273        return_type: Option<Type>,
274        /// Whether the function is async
275        is_async: bool,
276    },
277    /// Class or struct
278    Class {
279        /// Base classes
280        bases: Vec<NodeId>,
281        /// Implemented interfaces
282        interfaces: Vec<NodeId>,
283    },
284    /// Module or namespace
285    Module {
286        /// Exported symbols
287        exports: Vec<NodeId>,
288    },
289    /// Variable, constant, or field
290    Variable {
291        /// Variable type (if known)
292        var_type: Option<Type>,
293    },
294}
295
296/// Function parameter
297#[derive(Debug, Clone, PartialEq)]
298pub struct Param {
299    /// Parameter name
300    pub name: String,
301    /// Parameter type (if known)
302    pub param_type: Option<Type>,
303}
304
305/// Type information (simplified for now)
306#[derive(Debug, Clone, PartialEq)]
307pub struct Type {
308    /// Type name
309    pub name: String,
310}
311
312/// Additional metadata for a node
313#[derive(Debug, Clone, Default)]
314pub struct NodeMetadata {
315    /// Visibility (public, private, etc.)
316    pub visibility: Option<String>,
317    /// Documentation string
318    pub doc_comment: Option<String>,
319    /// Attributes/decorators
320    pub attributes: Vec<String>,
321}
322
323/// A node in the code graph representing a code entity
324#[derive(Debug, Clone)]
325pub struct CodeNode {
326    /// Unique identifier
327    pub id: NodeId,
328    /// Node type (function, class, module, etc.)
329    pub kind: NodeKind,
330    /// Source location
331    pub span: Span,
332    /// Additional metadata
333    pub metadata: NodeMetadata,
334}
335
336#[cfg(test)]
337mod tests {
338    use super::*;
339
340    #[test]
341    fn test_node_id_creation() {
342        let id = NodeId::new(Language::Cpp, "src/main.cpp", "main");
343        assert_eq!(id.language, Language::Cpp);
344        assert_eq!(id.file.as_ref(), "src/main.cpp");
345        assert_eq!(id.qualified_name.as_ref(), "main");
346    }
347
348    #[test]
349    fn test_node_id_display() {
350        let id = NodeId::new(Language::Python, "api.py", "User.authenticate");
351        assert_eq!(id.to_string(), "py:api.py:User.authenticate");
352    }
353
354    #[test]
355    fn test_node_id_hash() {
356        use std::collections::HashSet;
357
358        let id1 = NodeId::new(Language::JavaScript, "api.js", "fetchUsers");
359        let id2 = NodeId::new(Language::JavaScript, "api.js", "fetchUsers");
360        let id3 = NodeId::new(Language::JavaScript, "api.js", "createUser");
361
362        let mut set = HashSet::new();
363        set.insert(id1.clone());
364        set.insert(id2.clone());
365        set.insert(id3.clone());
366
367        assert_eq!(set.len(), 2); // id1 and id2 are equal
368    }
369
370    #[test]
371    fn test_node_id_clone_cheap() {
372        let id1 = NodeId::new(Language::Cpp, "src/utils.cpp", "std::vector::push_back");
373        let id2 = id1.clone();
374
375        // Arc<str> means the underlying string is NOT copied
376        assert_eq!(Arc::as_ptr(&id1.file), Arc::as_ptr(&id2.file));
377        assert_eq!(
378            Arc::as_ptr(&id1.qualified_name),
379            Arc::as_ptr(&id2.qualified_name)
380        );
381    }
382
383    #[test]
384    fn test_symbol_name_extraction() {
385        let id1 = NodeId::new(Language::Cpp, "main.cpp", "std::vector::push_back");
386        assert_eq!(id1.symbol_name(), "push_back");
387
388        let id2 = NodeId::new(Language::Python, "api.py", "User.authenticate");
389        assert_eq!(id2.symbol_name(), "authenticate");
390
391        let id3 = NodeId::new(Language::JavaScript, "api.js", "fetchUsers");
392        assert_eq!(id3.symbol_name(), "fetchUsers");
393    }
394
395    #[test]
396    fn test_span_creation() {
397        let span = Span::new(Position::new(10, 0), Position::new(20, 1));
398
399        assert_eq!(span.start.line, 10);
400        assert_eq!(span.end.line, 20);
401    }
402
403    #[test]
404    fn test_language_display() {
405        assert_eq!(Language::Cpp.to_string(), "cpp");
406        assert_eq!(Language::JavaScript.to_string(), "js");
407        assert_eq!(Language::Python.to_string(), "py");
408        assert_eq!(Language::Ruby.to_string(), "ruby");
409        assert_eq!(Language::Php.to_string(), "php");
410        assert_eq!(Language::Swift.to_string(), "swift");
411        assert_eq!(Language::Kotlin.to_string(), "kotlin");
412        assert_eq!(Language::Scala.to_string(), "scala");
413        assert_eq!(Language::Http.to_string(), "http");
414    }
415}