Skip to main content

sqry_core/graph/unified/node/
kind.rs

1//! `NodeKind` enumeration for the unified graph architecture.
2//!
3//! This module defines `NodeKind`, which categorizes all code entities
4//! that can be represented as nodes in the graph.
5//!
6//! # Design (, Appendix A1)
7//!
8//! The enumeration covers:
9//! - **Core symbols**: Functions, methods, classes, interfaces, traits
10//! - **Declarations**: Variables, constants, types, modules
11//! - **Structural**: Call sites, components, services
12//! - **Domain-specific**: Resources, endpoints, handlers
13
14use std::fmt;
15
16use serde::{Deserialize, Serialize};
17
18/// Enumeration of code entity types that can be represented as graph nodes.
19///
20/// Each variant represents a distinct category of code symbol. The categorization
21/// is language-agnostic to support cross-language analysis.
22///
23/// # Serialization
24///
25/// All variants serialize to their lowercase name for JSON interoperability:
26/// - `Function` → `"function"`
27/// - `CallSite` → `"call_site"`
28#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
29#[serde(rename_all = "snake_case")]
30pub enum NodeKind {
31    // ==================== Core Symbols ====================
32    /// A standalone function (not a method).
33    Function,
34
35    /// A method belonging to a class, struct, or trait.
36    Method,
37
38    /// A class definition (OOP languages).
39    Class,
40
41    /// An interface definition (TypeScript, Go, Java, etc.).
42    Interface,
43
44    /// A trait definition (Rust, Scala, etc.).
45    Trait,
46
47    /// A module or namespace declaration.
48    Module,
49
50    // ==================== Declarations ====================
51    /// A variable binding (let, var, const in some languages).
52    Variable,
53
54    /// A constant value (const, static const, final).
55    Constant,
56
57    /// A type alias or typedef.
58    Type,
59
60    /// A struct definition.
61    Struct,
62
63    /// An enum definition.
64    Enum,
65
66    /// An enum variant.
67    EnumVariant,
68
69    /// A macro definition.
70    Macro,
71
72    /// A function parameter.
73    Parameter,
74
75    /// A class property or struct field.
76    Property,
77
78    // ==================== Structural ====================
79    /// A call site (location where a call occurs).
80    ///
81    /// Call sites are used for fine-grained call graph analysis,
82    /// allowing multiple edges from the same function.
83    CallSite,
84
85    /// An import statement or declaration.
86    Import,
87
88    /// An export statement or re-export.
89    Export,
90
91    // ==================== Styles (CSS/SCSS/Less) ====================
92    /// A style rule (selector block).
93    StyleRule,
94
95    /// A style at-rule (@media, @keyframes, etc.).
96    StyleAtRule,
97
98    /// A style variable ($var or --var).
99    StyleVariable,
100
101    // ==================== Rust-Specific ====================
102    /// A lifetime parameter pseudo-symbol.
103    ///
104    /// Lifetime nodes enable `LifetimeConstraint` edges to have proper
105    /// source/target node IDs in the graph. Each lifetime parameter
106    /// (e.g., `'a`, `'b`, `'static`) in a function/struct signature
107    /// becomes a node.
108    ///
109    /// Qualified name format: `{owner}::'a` (e.g., `foo::'a` for `fn foo<'a>`)
110    /// Special case: `'static` is represented as `::static`
111    Lifetime,
112
113    // ==================== Domain-Specific ====================
114    /// A component (React, Vue, Angular, etc.).
115    Component,
116
117    /// A service class or service function.
118    Service,
119
120    /// A resource (REST, GraphQL, etc.).
121    Resource,
122
123    /// An API endpoint handler.
124    Endpoint,
125
126    /// A test function or test case.
127    Test,
128
129    // ==================== Extensibility ====================
130    /// A custom or language-specific node kind.
131    ///
132    /// Used for plugin-defined node types that don't fit other categories.
133    /// The string identifier provides semantic meaning.
134    #[serde(other)]
135    Other,
136}
137
138impl NodeKind {
139    /// Returns `true` if this is a callable entity (function, method).
140    #[inline]
141    #[must_use]
142    pub const fn is_callable(self) -> bool {
143        matches!(self, Self::Function | Self::Method | Self::Macro)
144    }
145
146    /// Returns `true` if this is a type definition.
147    #[inline]
148    #[must_use]
149    pub const fn is_type_definition(self) -> bool {
150        matches!(
151            self,
152            Self::Class | Self::Interface | Self::Trait | Self::Struct | Self::Enum | Self::Type
153        )
154    }
155
156    /// Returns `true` if this is a container that can have members.
157    #[inline]
158    #[must_use]
159    pub const fn is_container(self) -> bool {
160        matches!(
161            self,
162            Self::Class
163                | Self::Interface
164                | Self::Trait
165                | Self::Struct
166                | Self::Module
167                | Self::Enum
168                | Self::StyleRule
169                | Self::StyleAtRule
170        )
171    }
172
173    /// Returns `true` if this represents an import/export boundary.
174    #[inline]
175    #[must_use]
176    pub const fn is_boundary(self) -> bool {
177        matches!(self, Self::Import | Self::Export)
178    }
179
180    /// Returns `true` if this is a Rust lifetime pseudo-symbol.
181    #[inline]
182    #[must_use]
183    pub const fn is_lifetime(self) -> bool {
184        matches!(self, Self::Lifetime)
185    }
186
187    /// Returns the canonical string representation for serialization.
188    #[must_use]
189    pub const fn as_str(self) -> &'static str {
190        match self {
191            Self::Function => "function",
192            Self::Method => "method",
193            Self::Class => "class",
194            Self::Interface => "interface",
195            Self::Trait => "trait",
196            Self::Module => "module",
197            Self::Variable => "variable",
198            Self::Constant => "constant",
199            Self::Type => "type",
200            Self::Struct => "struct",
201            Self::Enum => "enum",
202            Self::EnumVariant => "enum_variant",
203            Self::Macro => "macro",
204            Self::Parameter => "parameter",
205            Self::Property => "property",
206            Self::CallSite => "call_site",
207            Self::Import => "import",
208            Self::Export => "export",
209            Self::StyleRule => "style_rule",
210            Self::StyleAtRule => "style_at_rule",
211            Self::StyleVariable => "style_variable",
212            Self::Lifetime => "lifetime",
213            Self::Component => "component",
214            Self::Service => "service",
215            Self::Resource => "resource",
216            Self::Endpoint => "endpoint",
217            Self::Test => "test",
218            Self::Other => "other",
219        }
220    }
221
222    /// Parses a string into a `NodeKind`.
223    ///
224    /// Returns `None` if the string doesn't match any known kind.
225    #[must_use]
226    pub fn parse(s: &str) -> Option<Self> {
227        match s {
228            "function" => Some(Self::Function),
229            "method" => Some(Self::Method),
230            "class" => Some(Self::Class),
231            "interface" => Some(Self::Interface),
232            "trait" => Some(Self::Trait),
233            "module" => Some(Self::Module),
234            "variable" => Some(Self::Variable),
235            "constant" => Some(Self::Constant),
236            "type" => Some(Self::Type),
237            "struct" => Some(Self::Struct),
238            "enum" => Some(Self::Enum),
239            "enum_variant" => Some(Self::EnumVariant),
240            "macro" => Some(Self::Macro),
241            "parameter" => Some(Self::Parameter),
242            "property" => Some(Self::Property),
243            "call_site" => Some(Self::CallSite),
244            "import" => Some(Self::Import),
245            "export" => Some(Self::Export),
246            "style_rule" => Some(Self::StyleRule),
247            "style_at_rule" => Some(Self::StyleAtRule),
248            "style_variable" => Some(Self::StyleVariable),
249            "lifetime" => Some(Self::Lifetime),
250            "component" => Some(Self::Component),
251            "service" => Some(Self::Service),
252            "resource" => Some(Self::Resource),
253            "endpoint" => Some(Self::Endpoint),
254            "test" => Some(Self::Test),
255            "other" => Some(Self::Other),
256            _ => None,
257        }
258    }
259}
260
261impl fmt::Display for NodeKind {
262    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
263        f.write_str(self.as_str())
264    }
265}
266
267impl Default for NodeKind {
268    /// Returns `NodeKind::Function` as the default (most common node type).
269    fn default() -> Self {
270        Self::Function
271    }
272}
273
274#[cfg(test)]
275mod tests {
276    use super::*;
277
278    #[test]
279    fn test_node_kind_as_str() {
280        assert_eq!(NodeKind::Function.as_str(), "function");
281        assert_eq!(NodeKind::Method.as_str(), "method");
282        assert_eq!(NodeKind::Class.as_str(), "class");
283        assert_eq!(NodeKind::CallSite.as_str(), "call_site");
284        assert_eq!(NodeKind::EnumVariant.as_str(), "enum_variant");
285    }
286
287    #[test]
288    fn test_node_kind_parse() {
289        assert_eq!(NodeKind::parse("function"), Some(NodeKind::Function));
290        assert_eq!(NodeKind::parse("call_site"), Some(NodeKind::CallSite));
291        assert_eq!(NodeKind::parse("unknown"), None);
292    }
293
294    #[test]
295    fn test_node_kind_display() {
296        assert_eq!(format!("{}", NodeKind::Function), "function");
297        assert_eq!(format!("{}", NodeKind::EnumVariant), "enum_variant");
298    }
299
300    #[test]
301    fn test_is_callable() {
302        assert!(NodeKind::Function.is_callable());
303        assert!(NodeKind::Method.is_callable());
304        assert!(NodeKind::Macro.is_callable());
305        assert!(!NodeKind::Class.is_callable());
306        assert!(!NodeKind::Variable.is_callable());
307    }
308
309    #[test]
310    fn test_is_type_definition() {
311        assert!(NodeKind::Class.is_type_definition());
312        assert!(NodeKind::Interface.is_type_definition());
313        assert!(NodeKind::Trait.is_type_definition());
314        assert!(NodeKind::Struct.is_type_definition());
315        assert!(NodeKind::Enum.is_type_definition());
316        assert!(NodeKind::Type.is_type_definition());
317        assert!(!NodeKind::Function.is_type_definition());
318        assert!(!NodeKind::Variable.is_type_definition());
319    }
320
321    #[test]
322    fn test_is_container() {
323        assert!(NodeKind::Class.is_container());
324        assert!(NodeKind::Module.is_container());
325        assert!(NodeKind::Struct.is_container());
326        assert!(!NodeKind::Function.is_container());
327        assert!(!NodeKind::Variable.is_container());
328    }
329
330    #[test]
331    fn test_is_boundary() {
332        assert!(NodeKind::Import.is_boundary());
333        assert!(NodeKind::Export.is_boundary());
334        assert!(!NodeKind::Function.is_boundary());
335    }
336
337    #[test]
338    fn test_is_lifetime() {
339        assert!(NodeKind::Lifetime.is_lifetime());
340        assert!(!NodeKind::Function.is_lifetime());
341        assert!(!NodeKind::Variable.is_lifetime());
342    }
343
344    #[test]
345    fn test_lifetime_serialization() {
346        assert_eq!(NodeKind::Lifetime.as_str(), "lifetime");
347        assert_eq!(NodeKind::parse("lifetime"), Some(NodeKind::Lifetime));
348
349        // JSON roundtrip
350        let json = serde_json::to_string(&NodeKind::Lifetime).unwrap();
351        assert_eq!(json, "\"lifetime\"");
352        let deserialized: NodeKind = serde_json::from_str(&json).unwrap();
353        assert_eq!(deserialized, NodeKind::Lifetime);
354    }
355
356    #[test]
357    fn test_default() {
358        assert_eq!(NodeKind::default(), NodeKind::Function);
359    }
360
361    #[test]
362    fn test_serde_roundtrip() {
363        let kinds = [
364            NodeKind::Function,
365            NodeKind::Method,
366            NodeKind::Class,
367            NodeKind::CallSite,
368            NodeKind::EnumVariant,
369        ];
370
371        for kind in kinds {
372            // JSON roundtrip
373            let json = serde_json::to_string(&kind).unwrap();
374            let deserialized: NodeKind = serde_json::from_str(&json).unwrap();
375            assert_eq!(kind, deserialized);
376
377            // Postcard roundtrip
378            let bytes = postcard::to_allocvec(&kind).unwrap();
379            let deserialized: NodeKind = postcard::from_bytes(&bytes).unwrap();
380            assert_eq!(kind, deserialized);
381        }
382    }
383
384    #[test]
385    fn test_json_format() {
386        // Verify snake_case serialization
387        assert_eq!(
388            serde_json::to_string(&NodeKind::Function).unwrap(),
389            "\"function\""
390        );
391        assert_eq!(
392            serde_json::to_string(&NodeKind::CallSite).unwrap(),
393            "\"call_site\""
394        );
395        assert_eq!(
396            serde_json::to_string(&NodeKind::EnumVariant).unwrap(),
397            "\"enum_variant\""
398        );
399    }
400
401    #[test]
402    fn test_hash() {
403        use std::collections::HashSet;
404
405        let mut set = HashSet::new();
406        set.insert(NodeKind::Function);
407        set.insert(NodeKind::Method);
408        set.insert(NodeKind::Class);
409
410        assert!(set.contains(&NodeKind::Function));
411        assert!(!set.contains(&NodeKind::Variable));
412        assert_eq!(set.len(), 3);
413    }
414
415    #[test]
416    #[allow(clippy::clone_on_copy)] // Intentionally testing Clone trait
417    fn test_copy_clone() {
418        let kind = NodeKind::Function;
419        let copied = kind;
420        let cloned = kind.clone();
421
422        assert_eq!(kind, copied);
423        assert_eq!(kind, cloned);
424    }
425}