Skip to main content

sqry_lang_cpp/
lib.rs

1//! C++ language plugin for sqry
2//!
3//! Implements the `LanguagePlugin` trait for C++, providing:
4//! - AST parsing with tree-sitter
5//! - Scope extraction
6//! - Relation extraction (call graph, includes/imports, exports)
7//!
8//! This plugin enables semantic code search for C++ codebases, the #3 priority
9//! language for systems programming and enterprise software.
10
11use sqry_core::ast::{Scope, ScopeId, link_nested_scopes};
12use sqry_core::plugin::{
13    LanguageMetadata, LanguagePlugin,
14    error::{ParseError, ScopeError},
15};
16use std::path::Path;
17use streaming_iterator::StreamingIterator;
18use tree_sitter::{Language, Parser, Query, QueryCursor, Tree};
19
20pub mod relations;
21
22/// C++ language plugin
23///
24/// Provides language support for C++ source files (.cpp, .cc, .cxx, .hpp, .h, .hh, .hxx).
25///
26/// # Example
27///
28/// ```
29/// use sqry_lang_cpp::CppPlugin;
30/// use sqry_core::plugin::LanguagePlugin;
31///
32/// let plugin = CppPlugin::new();
33/// let metadata = plugin.metadata();
34/// assert_eq!(metadata.id, "cpp");
35/// assert_eq!(metadata.name, "C++");
36/// ```
37pub struct CppPlugin {
38    graph_builder: relations::CppGraphBuilder,
39}
40
41impl CppPlugin {
42    #[must_use]
43    pub fn new() -> Self {
44        Self {
45            graph_builder: relations::CppGraphBuilder,
46        }
47    }
48}
49
50impl Default for CppPlugin {
51    fn default() -> Self {
52        Self::new()
53    }
54}
55
56impl LanguagePlugin for CppPlugin {
57    fn metadata(&self) -> LanguageMetadata {
58        LanguageMetadata {
59            id: "cpp",
60            name: "C++",
61            version: env!("CARGO_PKG_VERSION"),
62            author: "Verivus Pty Ltd",
63            description: "C++ language support for sqry - systems programming code search",
64            tree_sitter_version: "0.24",
65        }
66    }
67
68    fn extensions(&self) -> &'static [&'static str] {
69        &["cpp", "cc", "cxx", "hpp", "hh", "hxx"]
70    }
71
72    fn language(&self) -> Language {
73        tree_sitter_cpp::LANGUAGE.into()
74    }
75
76    fn parse_ast(&self, content: &[u8]) -> Result<Tree, ParseError> {
77        let mut parser = Parser::new();
78        let language = self.language();
79
80        parser.set_language(&language).map_err(|e| {
81            ParseError::LanguageSetFailed(format!("Failed to set C++ language: {e}"))
82        })?;
83
84        parser
85            .parse(content, None)
86            .ok_or(ParseError::TreeSitterFailed)
87    }
88
89    fn extract_scopes(
90        &self,
91        tree: &Tree,
92        content: &[u8],
93        file_path: &Path,
94    ) -> Result<Vec<Scope>, ScopeError> {
95        Self::extract_cpp_scopes(tree, content, file_path)
96    }
97
98    fn graph_builder(&self) -> Option<&dyn sqry_core::graph::GraphBuilder> {
99        Some(&self.graph_builder)
100    }
101}
102
103impl CppPlugin {
104    /// Tree-sitter query for C++ scope extraction
105    ///
106    /// # Grammar Limitations
107    ///
108    /// - **`function_try_block`**: C++ function-try-blocks like `void foo() try { } catch { }`
109    ///   are not supported because tree-sitter-cpp does not expose a `function_try_block`
110    ///   node type. These rare constructs (typically used in destructors to swallow
111    ///   exceptions) will not generate scopes. This is a grammar limitation, not an
112    ///   implementation limitation.
113    ///
114    /// - **defaulted/deleted special members** (both inline and out-of-class):
115    ///   All defaulted/deleted special member functions like `Resource() = default;`,
116    ///   `operator=(const Resource&) = delete;`, `Foo::Foo() = default;`, and
117    ///   `Foo::~Foo() = delete;` may not be captured. Tree-sitter-cpp parses these as
118    ///   `declaration` nodes rather than `function_definition` nodes, regardless of
119    ///   whether they appear inside a class body or at file scope.
120    ///   Methods with actual bodies (e.g., `void foo() {}`) are captured correctly.
121    ///   Patterns are included for `default_method_clause`/`delete_method_clause` bodies
122    ///   but coverage depends on actual tree-sitter-cpp parsing behavior.
123    #[allow(
124        clippy::too_many_lines,
125        reason = "Scope query enumerates C++ constructs in one consolidated query."
126    )]
127    fn scope_query_source() -> &'static str {
128        r"
129; Function definitions (with body) - at file/namespace scope
130(function_definition
131    declarator: (function_declarator
132        declarator: (identifier) @function.name)
133    body: (compound_statement)) @function.type
134
135; Function definitions with pointer return type
136(function_definition
137    declarator: (pointer_declarator
138        declarator: (function_declarator
139            declarator: (identifier) @function.name))
140    body: (compound_statement)) @function.type
141
142; Method definitions (qualified identifier - Class::method)
143(function_definition
144    declarator: (function_declarator
145        declarator: (qualified_identifier
146            name: (identifier) @method.name))
147    body: (compound_statement)) @method.type
148
149; Inline class method definitions (field_identifier inside class body)
150(function_definition
151    declarator: (function_declarator
152        declarator: (field_identifier) @method.name)
153    body: (compound_statement)) @method.type
154
155; Inline class method with = default (special member functions)
156(function_definition
157    declarator: (function_declarator
158        declarator: (field_identifier) @defaulted_method.name)
159    body: (default_method_clause)) @defaulted_method.type
160
161; Inline class method with = delete (deleted member functions)
162(function_definition
163    declarator: (function_declarator
164        declarator: (field_identifier) @deleted_method.name)
165    body: (delete_method_clause)) @deleted_method.type
166
167; Constructor with = default inside class body
168(function_definition
169    declarator: (function_declarator
170        declarator: (identifier) @defaulted_method.name)
171    body: (default_method_clause)) @defaulted_method.type
172
173; Constructor with = delete inside class body
174(function_definition
175    declarator: (function_declarator
176        declarator: (identifier) @deleted_method.name)
177    body: (delete_method_clause)) @deleted_method.type
178
179; Destructor with = default inside class body
180(function_definition
181    declarator: (function_declarator
182        declarator: (destructor_name) @defaulted_destructor.name)
183    body: (default_method_clause)) @defaulted_destructor.type
184
185; Destructor with = delete inside class body
186(function_definition
187    declarator: (function_declarator
188        declarator: (destructor_name) @deleted_destructor.name)
189    body: (delete_method_clause)) @deleted_destructor.type
190
191; Operator overload with = default (e.g., operator=() = default)
192(function_definition
193    declarator: (function_declarator
194        declarator: (operator_name) @defaulted_operator.name)
195    body: (default_method_clause)) @defaulted_operator.type
196
197; Operator overload with = delete (e.g., operator=() = delete)
198(function_definition
199    declarator: (function_declarator
200        declarator: (operator_name) @deleted_operator.name)
201    body: (delete_method_clause)) @deleted_operator.type
202
203; Out-of-class constructor with = default (Foo::Foo() = default)
204(function_definition
205    declarator: (function_declarator
206        declarator: (qualified_identifier
207            name: (identifier) @qualified_defaulted.name))
208    body: (default_method_clause)) @qualified_defaulted.type
209
210; Out-of-class constructor with = delete (Foo::Foo() = delete)
211(function_definition
212    declarator: (function_declarator
213        declarator: (qualified_identifier
214            name: (identifier) @qualified_deleted.name))
215    body: (delete_method_clause)) @qualified_deleted.type
216
217; Out-of-class destructor with = default (Foo::~Foo() = default)
218(function_definition
219    declarator: (function_declarator
220        declarator: (qualified_identifier
221            name: (destructor_name) @qualified_defaulted_destructor.name))
222    body: (default_method_clause)) @qualified_defaulted_destructor.type
223
224; Out-of-class destructor with = delete (Foo::~Foo() = delete)
225(function_definition
226    declarator: (function_declarator
227        declarator: (qualified_identifier
228            name: (destructor_name) @qualified_deleted_destructor.name))
229    body: (delete_method_clause)) @qualified_deleted_destructor.type
230
231; Out-of-class operator with = default (Foo::operator=() = default)
232(function_definition
233    declarator: (function_declarator
234        declarator: (qualified_identifier
235            name: (operator_name) @qualified_defaulted_operator.name))
236    body: (default_method_clause)) @qualified_defaulted_operator.type
237
238; Out-of-class operator with = delete (Foo::operator=() = delete)
239(function_definition
240    declarator: (function_declarator
241        declarator: (qualified_identifier
242            name: (operator_name) @qualified_deleted_operator.name))
243    body: (delete_method_clause)) @qualified_deleted_operator.type
244
245; Destructor definitions (qualified identifier - Class::~Class)
246(function_definition
247    declarator: (function_declarator
248        declarator: (qualified_identifier
249            name: (destructor_name) @destructor.name))
250    body: (compound_statement)) @destructor.type
251
252; Destructor defined inside class body (inline destructor)
253(function_definition
254    declarator: (function_declarator
255        declarator: (destructor_name) @destructor.name)
256    body: (compound_statement)) @destructor.type
257
258; Class definitions with body
259(class_specifier
260    name: (type_identifier) @class.name
261    body: (field_declaration_list)) @class.type
262
263; Struct definitions with body
264(struct_specifier
265    name: (type_identifier) @struct.name
266    body: (field_declaration_list)) @struct.type
267
268; Enum definitions with body
269(enum_specifier
270    name: (type_identifier) @enum.name
271    body: (enumerator_list)) @enum.type
272
273; Scoped enum (enum class) with body
274(enum_specifier
275    name: (type_identifier) @enum.name
276    body: (enumerator_list)) @enum.type
277
278; Union definitions with body
279(union_specifier
280    name: (type_identifier) @union.name
281    body: (field_declaration_list)) @union.type
282
283; Namespace definitions with body
284(namespace_definition
285    name: (namespace_identifier) @namespace.name
286    body: (declaration_list)) @namespace.type
287
288; Lambda expressions (anonymous functions)
289(lambda_expression
290    body: (compound_statement)) @lambda.type
291"
292    }
293
294    /// Extract scopes from C++ source using tree-sitter queries
295    #[allow(
296        clippy::too_many_lines,
297        reason = "Scope extraction matches many capture variants in a single pass."
298    )]
299    fn extract_cpp_scopes(
300        tree: &Tree,
301        content: &[u8],
302        file_path: &Path,
303    ) -> Result<Vec<Scope>, ScopeError> {
304        let root_node = tree.root_node();
305        let language: Language = tree_sitter_cpp::LANGUAGE.into();
306        let scope_query = Self::scope_query_source();
307
308        let query = Query::new(&language, scope_query)
309            .map_err(|e| ScopeError::QueryCompilationFailed(e.to_string()))?;
310
311        let mut scopes = Vec::new();
312        let mut cursor = QueryCursor::new();
313        let mut query_matches = cursor.matches(&query, root_node, content);
314
315        while let Some(m) = query_matches.next() {
316            let mut scope_type: Option<&str> = None;
317            let mut scope_name: Option<String> = None;
318            let mut type_node: Option<tree_sitter::Node> = None;
319
320            for capture in m.captures {
321                let capture_name = query.capture_names()[capture.index as usize];
322                match capture_name {
323                    "function.type"
324                    | "method.type"
325                    | "class.type"
326                    | "struct.type"
327                    | "namespace.type"
328                    | "destructor.type"
329                    | "enum.type"
330                    | "union.type"
331                    | "lambda.type"
332                    | "defaulted_method.type"
333                    | "deleted_method.type"
334                    | "defaulted_destructor.type"
335                    | "deleted_destructor.type"
336                    | "defaulted_operator.type"
337                    | "deleted_operator.type"
338                    | "qualified_defaulted.type"
339                    | "qualified_deleted.type"
340                    | "qualified_defaulted_destructor.type"
341                    | "qualified_deleted_destructor.type"
342                    | "qualified_defaulted_operator.type"
343                    | "qualified_deleted_operator.type" => {
344                        // Map defaulted/deleted/operator variants to their base scope types
345                        let type_name = match capture_name {
346                            "defaulted_method.type"
347                            | "deleted_method.type"
348                            | "qualified_defaulted.type"
349                            | "qualified_deleted.type"
350                            | "defaulted_operator.type"
351                            | "deleted_operator.type"
352                            | "qualified_defaulted_operator.type"
353                            | "qualified_deleted_operator.type" => "method",
354                            "defaulted_destructor.type"
355                            | "deleted_destructor.type"
356                            | "qualified_defaulted_destructor.type"
357                            | "qualified_deleted_destructor.type" => "destructor",
358                            _ => capture_name.split('.').next().unwrap_or("unknown"),
359                        };
360                        scope_type = Some(type_name);
361                        type_node = Some(capture.node);
362                    }
363                    "function.name"
364                    | "method.name"
365                    | "class.name"
366                    | "struct.name"
367                    | "namespace.name"
368                    | "destructor.name"
369                    | "enum.name"
370                    | "union.name"
371                    | "defaulted_method.name"
372                    | "deleted_method.name"
373                    | "defaulted_destructor.name"
374                    | "deleted_destructor.name"
375                    | "defaulted_operator.name"
376                    | "deleted_operator.name"
377                    | "qualified_defaulted.name"
378                    | "qualified_deleted.name"
379                    | "qualified_defaulted_destructor.name"
380                    | "qualified_deleted_destructor.name"
381                    | "qualified_defaulted_operator.name"
382                    | "qualified_deleted_operator.name" => {
383                        scope_name = capture.node.utf8_text(content).ok().map(String::from);
384                    }
385                    _ => {}
386                }
387            }
388
389            // Handle lambda expressions which don't have explicit names
390            if scope_type == Some("lambda") && scope_name.is_none() {
391                scope_name = Some("<lambda>".to_string());
392            }
393
394            if let (Some(scope_type_str), Some(name), Some(node)) =
395                (scope_type, scope_name, type_node)
396            {
397                let start_pos = node.start_position();
398                let end_pos = node.end_position();
399
400                scopes.push(Scope {
401                    id: ScopeId::new(0),
402                    name,
403                    scope_type: scope_type_str.to_string(),
404                    file_path: file_path.to_path_buf(),
405                    start_line: start_pos.row + 1,
406                    start_column: start_pos.column,
407                    end_line: end_pos.row + 1,
408                    end_column: end_pos.column,
409                    parent_id: None,
410                });
411            }
412        }
413
414        // Deduplicate by (name, start_line, start_column) - some patterns may match same scope
415        scopes.sort_by_key(|s| (s.name.clone(), s.start_line, s.start_column));
416        scopes.dedup_by(|a, b| {
417            a.name == b.name && a.start_line == b.start_line && a.start_column == b.start_column
418        });
419
420        // Sort by position and link nested scopes
421        scopes.sort_by_key(|s| (s.start_line, s.start_column));
422        link_nested_scopes(&mut scopes);
423
424        Ok(scopes)
425    }
426}
427
428#[cfg(test)]
429mod tests {
430    use super::*;
431
432    #[test]
433    fn test_metadata() {
434        let plugin = CppPlugin::new();
435        let metadata = plugin.metadata();
436
437        assert_eq!(metadata.id, "cpp");
438        assert_eq!(metadata.name, "C++");
439        assert_eq!(metadata.version, env!("CARGO_PKG_VERSION"));
440        assert_eq!(metadata.author, "Verivus Pty Ltd");
441        assert_eq!(metadata.tree_sitter_version, "0.24");
442    }
443
444    #[test]
445    fn test_extensions() {
446        let plugin = CppPlugin::new();
447        let extensions = plugin.extensions();
448
449        assert_eq!(extensions.len(), 6);
450        assert!(extensions.contains(&"cpp"));
451        assert!(extensions.contains(&"hpp"));
452        assert!(extensions.contains(&"hh"));
453    }
454
455    #[test]
456    fn test_language() {
457        let plugin = CppPlugin::new();
458        let language = plugin.language();
459
460        // Just verify we can get a language (ABI version should be non-zero)
461        assert!(language.abi_version() > 0);
462    }
463
464    #[test]
465    fn test_parse_ast_simple() {
466        let plugin = CppPlugin::new();
467        let source = b"class MyClass {};";
468
469        let tree = plugin.parse_ast(source).unwrap();
470        assert!(!tree.root_node().has_error());
471    }
472
473    #[test]
474    fn test_plugin_is_send_sync() {
475        fn assert_send_sync<T: Send + Sync>() {}
476        assert_send_sync::<CppPlugin>();
477    }
478
479    #[test]
480    fn test_extract_scopes_functions() {
481        let plugin = CppPlugin::new();
482        let source = br#"
483void foo() {
484    int x = 1;
485}
486
487int main(int argc, char** argv) {
488    foo();
489    return 0;
490}
491"#;
492        let path = std::path::Path::new("test.cpp");
493        let tree = plugin.parse_ast(source).unwrap();
494        let scopes = plugin.extract_scopes(&tree, source, path).unwrap();
495
496        // Should have 2 function scopes: foo and main
497        assert_eq!(
498            scopes.len(),
499            2,
500            "Expected 2 function scopes, got {}",
501            scopes.len()
502        );
503
504        // Verify scope names and types
505        let scope_names: Vec<&str> = scopes.iter().map(|s| s.name.as_str()).collect();
506        assert!(scope_names.contains(&"foo"), "Missing 'foo' scope");
507        assert!(scope_names.contains(&"main"), "Missing 'main' scope");
508
509        // All should be function type
510        for scope in &scopes {
511            assert_eq!(scope.scope_type, "function", "Expected function scope type");
512        }
513    }
514
515    #[test]
516    fn test_extract_scopes_class() {
517        let plugin = CppPlugin::new();
518        let source = br#"
519class MyClass {
520public:
521    void method();
522    int value;
523};
524
525void MyClass::method() {
526    value = 42;
527}
528"#;
529        let path = std::path::Path::new("test.cpp");
530        let tree = plugin.parse_ast(source).unwrap();
531        let scopes = plugin.extract_scopes(&tree, source, path).unwrap();
532
533        // Should have 2 scopes: MyClass (class) and method (method)
534        assert_eq!(scopes.len(), 2, "Expected 2 scopes, got {}", scopes.len());
535
536        let class_scope = scopes.iter().find(|s| s.name == "MyClass");
537        let method_scope = scopes.iter().find(|s| s.name == "method");
538
539        assert!(class_scope.is_some(), "Missing 'MyClass' class scope");
540        assert!(method_scope.is_some(), "Missing 'method' scope");
541
542        assert_eq!(class_scope.unwrap().scope_type, "class");
543        assert_eq!(method_scope.unwrap().scope_type, "method");
544    }
545
546    #[test]
547    fn test_extract_scopes_namespace() {
548        let plugin = CppPlugin::new();
549        let source = br#"
550namespace demo {
551    void helper() {
552        // helper implementation
553    }
554}
555"#;
556        let path = std::path::Path::new("test.cpp");
557        let tree = plugin.parse_ast(source).unwrap();
558        let scopes = plugin.extract_scopes(&tree, source, path).unwrap();
559
560        // Should have 2 scopes: demo (namespace) and helper (function)
561        assert_eq!(scopes.len(), 2, "Expected 2 scopes, got {}", scopes.len());
562
563        let ns_scope = scopes.iter().find(|s| s.name == "demo");
564        let func_scope = scopes.iter().find(|s| s.name == "helper");
565
566        assert!(ns_scope.is_some(), "Missing 'demo' namespace scope");
567        assert!(func_scope.is_some(), "Missing 'helper' function scope");
568
569        assert_eq!(ns_scope.unwrap().scope_type, "namespace");
570        assert_eq!(func_scope.unwrap().scope_type, "function");
571
572        // helper should be nested inside demo namespace
573        let demo = ns_scope.unwrap();
574        let helper = func_scope.unwrap();
575        assert!(
576            helper.start_line > demo.start_line,
577            "helper should be inside demo"
578        );
579        assert!(
580            helper.end_line < demo.end_line,
581            "helper should be inside demo"
582        );
583    }
584
585    #[test]
586    fn test_extract_scopes_struct() {
587        let plugin = CppPlugin::new();
588        let source = br#"
589struct Point {
590    int x;
591    int y;
592};
593"#;
594        let path = std::path::Path::new("test.cpp");
595        let tree = plugin.parse_ast(source).unwrap();
596        let scopes = plugin.extract_scopes(&tree, source, path).unwrap();
597
598        // Should have 1 struct scope: Point
599        assert_eq!(
600            scopes.len(),
601            1,
602            "Expected 1 struct scope, got {}",
603            scopes.len()
604        );
605        assert_eq!(scopes[0].name, "Point");
606        assert_eq!(scopes[0].scope_type, "struct");
607    }
608
609    #[test]
610    fn test_extract_scopes_destructor() {
611        let plugin = CppPlugin::new();
612        let source = br#"
613class Resource {
614public:
615    ~Resource() {
616        // cleanup
617    }
618};
619"#;
620        let path = std::path::Path::new("test.cpp");
621        let tree = plugin.parse_ast(source).unwrap();
622        let scopes = plugin.extract_scopes(&tree, source, path).unwrap();
623
624        // Should have 2 scopes: Resource (class) and ~Resource (destructor)
625        assert_eq!(scopes.len(), 2, "Expected 2 scopes, got {}", scopes.len());
626
627        let class_scope = scopes.iter().find(|s| s.name == "Resource");
628        let destructor_scope = scopes.iter().find(|s| s.name == "~Resource");
629
630        assert!(class_scope.is_some(), "Missing 'Resource' class scope");
631        assert!(
632            destructor_scope.is_some(),
633            "Missing '~Resource' destructor scope"
634        );
635
636        assert_eq!(class_scope.unwrap().scope_type, "class");
637        assert_eq!(destructor_scope.unwrap().scope_type, "destructor");
638    }
639
640    #[test]
641    fn test_extract_scopes_enum() {
642        let plugin = CppPlugin::new();
643        let source = br#"
644enum Color {
645    Red,
646    Green,
647    Blue
648};
649
650enum class Status {
651    Active,
652    Inactive
653};
654"#;
655        let path = std::path::Path::new("test.cpp");
656        let tree = plugin.parse_ast(source).unwrap();
657        let scopes = plugin.extract_scopes(&tree, source, path).unwrap();
658
659        // Should have 2 enum scopes: Color and Status
660        assert_eq!(
661            scopes.len(),
662            2,
663            "Expected 2 enum scopes, got {}",
664            scopes.len()
665        );
666
667        let color_scope = scopes.iter().find(|s| s.name == "Color");
668        let status_scope = scopes.iter().find(|s| s.name == "Status");
669
670        assert!(color_scope.is_some(), "Missing 'Color' enum scope");
671        assert!(status_scope.is_some(), "Missing 'Status' enum scope");
672
673        assert_eq!(color_scope.unwrap().scope_type, "enum");
674        assert_eq!(status_scope.unwrap().scope_type, "enum");
675    }
676
677    #[test]
678    fn test_extract_scopes_union() {
679        let plugin = CppPlugin::new();
680        let source = br#"
681union Value {
682    int i;
683    float f;
684    char c;
685};
686"#;
687        let path = std::path::Path::new("test.cpp");
688        let tree = plugin.parse_ast(source).unwrap();
689        let scopes = plugin.extract_scopes(&tree, source, path).unwrap();
690
691        // Should have 1 union scope: Value
692        assert_eq!(
693            scopes.len(),
694            1,
695            "Expected 1 union scope, got {}",
696            scopes.len()
697        );
698        assert_eq!(scopes[0].name, "Value");
699        assert_eq!(scopes[0].scope_type, "union");
700    }
701
702    #[test]
703    fn test_extract_scopes_lambda() {
704        let plugin = CppPlugin::new();
705        let source = br#"
706void process() {
707    auto callback = [](int x) {
708        return x * 2;
709    };
710}
711"#;
712        let path = std::path::Path::new("test.cpp");
713        let tree = plugin.parse_ast(source).unwrap();
714        let scopes = plugin.extract_scopes(&tree, source, path).unwrap();
715
716        // Should have 2 scopes: process (function) and <lambda>
717        assert_eq!(scopes.len(), 2, "Expected 2 scopes, got {}", scopes.len());
718
719        let func_scope = scopes.iter().find(|s| s.name == "process");
720        let lambda_scope = scopes.iter().find(|s| s.name == "<lambda>");
721
722        assert!(func_scope.is_some(), "Missing 'process' function scope");
723        assert!(lambda_scope.is_some(), "Missing '<lambda>' scope");
724
725        assert_eq!(func_scope.unwrap().scope_type, "function");
726        assert_eq!(lambda_scope.unwrap().scope_type, "lambda");
727    }
728
729    #[test]
730    fn test_extract_scopes_inline_class_methods() {
731        let plugin = CppPlugin::new();
732        let source = br#"
733class Foo {
734    void bar() {
735        // inline method defined inside class body
736    }
737
738    int getValue() {
739        return 42;
740    }
741};
742"#;
743        let path = std::path::Path::new("test.cpp");
744        let tree = plugin.parse_ast(source).unwrap();
745        let scopes = plugin.extract_scopes(&tree, source, path).unwrap();
746
747        // Should have 3 scopes: Foo (class), bar (method), getValue (method)
748        assert_eq!(
749            scopes.len(),
750            3,
751            "Expected 3 scopes, got {}: {:?}",
752            scopes.len(),
753            scopes
754                .iter()
755                .map(|s| (&s.name, &s.scope_type))
756                .collect::<Vec<_>>()
757        );
758
759        let class_scope = scopes.iter().find(|s| s.name == "Foo");
760        let bar_scope = scopes.iter().find(|s| s.name == "bar");
761        let get_value_scope = scopes.iter().find(|s| s.name == "getValue");
762
763        assert!(class_scope.is_some(), "Missing 'Foo' class scope");
764        assert!(bar_scope.is_some(), "Missing 'bar' inline method scope");
765        assert!(
766            get_value_scope.is_some(),
767            "Missing 'getValue' inline method scope"
768        );
769
770        assert_eq!(class_scope.unwrap().scope_type, "class");
771        assert_eq!(bar_scope.unwrap().scope_type, "method");
772        assert_eq!(get_value_scope.unwrap().scope_type, "method");
773
774        // Verify parent_id nesting: class has no parent, methods have class as parent
775        assert!(
776            class_scope.unwrap().parent_id.is_none(),
777            "Top-level class Foo should have no parent"
778        );
779        assert!(
780            bar_scope.unwrap().parent_id.is_some(),
781            "Method 'bar' should have parent_id pointing to class Foo"
782        );
783        assert!(
784            get_value_scope.unwrap().parent_id.is_some(),
785            "Method 'getValue' should have parent_id pointing to class Foo"
786        );
787    }
788
789    #[test]
790    fn test_extract_scopes_defaulted_deleted_methods() {
791        let plugin = CppPlugin::new();
792        let source = br#"
793class Resource {
794    Resource() = default;
795    ~Resource() = default;
796    Resource(const Resource&) = delete;
797    Resource& operator=(const Resource&) = delete;
798    void process() {}
799};
800"#;
801        let path = std::path::Path::new("test.cpp");
802        let tree = plugin.parse_ast(source).unwrap();
803        let scopes = plugin.extract_scopes(&tree, source, path).unwrap();
804
805        // Print all scopes for debugging
806        eprintln!("All scopes found:");
807        for scope in &scopes {
808            eprintln!(
809                "  - name: '{}', type: '{}', parent_id: {:?}",
810                scope.name, scope.scope_type, scope.parent_id
811            );
812        }
813
814        // Should have scopes for: Resource (class), Resource constructor (defaulted),
815        // ~Resource destructor (defaulted), Resource copy ctor (deleted),
816        // operator= (deleted), process (method)
817        let class_scope = scopes
818            .iter()
819            .find(|s| s.name == "Resource" && s.scope_type == "class");
820        let process_scope = scopes.iter().find(|s| s.name == "process");
821
822        // Find defaulted/deleted special member functions
823        let default_ctor = scopes
824            .iter()
825            .find(|s| s.name == "Resource" && s.scope_type == "method");
826        let default_dtor = scopes
827            .iter()
828            .find(|s| s.name.contains('~') && s.scope_type == "destructor");
829        let deleted_operator = scopes
830            .iter()
831            .find(|s| s.name.contains("operator") && s.scope_type == "method");
832
833        assert!(class_scope.is_some(), "Missing 'Resource' class scope");
834        assert!(process_scope.is_some(), "Missing 'process' method scope");
835        assert_eq!(process_scope.unwrap().scope_type, "method");
836
837        // Verify parent_id nesting - class has no parent
838        let class_scope = class_scope.unwrap();
839        assert!(
840            class_scope.parent_id.is_none(),
841            "Top-level class should have no parent"
842        );
843
844        // Verify process method has the class as parent (check actual parent_id value)
845        let process_scope = process_scope.unwrap();
846        assert!(
847            process_scope.parent_id.is_some(),
848            "Method 'process' should have parent_id pointing to class"
849        );
850        assert_eq!(
851            process_scope.parent_id,
852            Some(class_scope.id),
853            "Method 'process' parent_id should match class id ({:?})",
854            class_scope.id
855        );
856
857        // NOTE: Inline defaulted/deleted special member functions (inside class body) are
858        // parsed as `declaration` nodes by tree-sitter-cpp, NOT `function_definition` nodes.
859        // This is a documented grammar limitation. The `if let` pattern is intentional because
860        // we cannot guarantee these will be captured by our queries.
861        //
862        // These assertions verify correct behavior IF the scopes are captured (e.g., if
863        // tree-sitter-cpp grammar changes in the future to parse them as function_definition).
864        if let Some(ctor) = default_ctor {
865            assert_eq!(
866                ctor.scope_type, "method",
867                "Defaulted constructor should be method type"
868            );
869            assert_eq!(
870                ctor.parent_id,
871                Some(class_scope.id),
872                "Defaulted constructor parent_id should match class id"
873            );
874        }
875
876        if let Some(dtor) = default_dtor {
877            assert_eq!(
878                dtor.scope_type, "destructor",
879                "Defaulted destructor should be destructor type"
880            );
881            assert_eq!(
882                dtor.parent_id,
883                Some(class_scope.id),
884                "Defaulted destructor parent_id should match class id"
885            );
886        }
887
888        if let Some(op) = deleted_operator {
889            assert_eq!(
890                op.scope_type, "method",
891                "Deleted operator= should be method type"
892            );
893            assert_eq!(
894                op.parent_id,
895                Some(class_scope.id),
896                "Deleted operator= parent_id should match class id"
897            );
898        }
899    }
900
901    #[test]
902    fn test_extract_scopes_out_of_class_defaulted() {
903        let plugin = CppPlugin::new();
904        let source = br#"
905class Foo {
906    Foo();
907    ~Foo();
908};
909
910// Out-of-class defaulted special members
911Foo::Foo() = default;
912Foo::~Foo() = default;
913"#;
914        let path = std::path::Path::new("test.cpp");
915        let tree = plugin.parse_ast(source).unwrap();
916        let scopes = plugin.extract_scopes(&tree, source, path).unwrap();
917
918        // Print all scopes for debugging
919        eprintln!("All out-of-class scopes found:");
920        for scope in &scopes {
921            eprintln!(
922                "  - name: '{}', type: '{}', line: {}",
923                scope.name, scope.scope_type, scope.start_line
924            );
925        }
926
927        // Should have the class
928        let class_scope = scopes
929            .iter()
930            .find(|s| s.name == "Foo" && s.scope_type == "class");
931        assert!(class_scope.is_some(), "Missing 'Foo' class scope");
932
933        // NOTE: Out-of-class defaulted/deleted definitions like `Foo::Foo() = default;`
934        // are ALSO parsed as `declaration` nodes by tree-sitter-cpp, NOT `function_definition`.
935        // This is the same grammar limitation as inline defaulted/deleted methods.
936        // The patterns are correct and will capture these if tree-sitter-cpp ever parses
937        // them as function_definition nodes.
938        //
939        // Test verifies behavior IF captured (future-proofing for grammar changes).
940        let out_of_class_ctor = scopes
941            .iter()
942            .find(|s| s.scope_type == "method" && s.start_line > 6);
943        let out_of_class_dtor = scopes
944            .iter()
945            .find(|s| s.scope_type == "destructor" && s.start_line > 6);
946
947        if let Some(ctor) = out_of_class_ctor {
948            assert_eq!(
949                ctor.scope_type, "method",
950                "Out-of-class ctor should be method type"
951            );
952            assert_eq!(ctor.name, "Foo", "Out-of-class ctor name should be 'Foo'");
953            eprintln!("Successfully captured out-of-class defaulted constructor");
954        }
955
956        if let Some(dtor) = out_of_class_dtor {
957            assert_eq!(
958                dtor.scope_type, "destructor",
959                "Out-of-class dtor should be destructor type"
960            );
961            // Destructor name may include the tilde or just 'Foo' depending on extraction
962            assert!(
963                dtor.name.contains('~') || dtor.name == "Foo",
964                "Out-of-class dtor name should contain '~' or be 'Foo', got: '{}'",
965                dtor.name
966            );
967            eprintln!("Successfully captured out-of-class defaulted destructor");
968        }
969    }
970
971    #[test]
972    fn test_extract_scopes_out_of_class_qualified_operators() {
973        let plugin = CppPlugin::new();
974        let source = br#"
975class Foo {
976    Foo& operator=(const Foo&);
977    bool operator==(const Foo&) const;
978};
979
980// Out-of-class qualified operators with = default/delete
981Foo& Foo::operator=(const Foo&) = default;
982bool Foo::operator==(const Foo&) const = delete;
983"#;
984        let path = std::path::Path::new("test.cpp");
985        let tree = plugin.parse_ast(source).unwrap();
986        let scopes = plugin.extract_scopes(&tree, source, path).unwrap();
987
988        // Print all scopes for debugging
989        eprintln!("All out-of-class qualified operator scopes found:");
990        for scope in &scopes {
991            eprintln!(
992                "  - name: '{}', type: '{}', line: {}",
993                scope.name, scope.scope_type, scope.start_line
994            );
995        }
996
997        // Should have the class
998        let class_scope = scopes
999            .iter()
1000            .find(|s| s.name == "Foo" && s.scope_type == "class");
1001        assert!(class_scope.is_some(), "Missing 'Foo' class scope");
1002
1003        // NOTE: Out-of-class qualified operators like `Foo::operator=(const Foo&) = default;`
1004        // are parsed as `declaration` nodes by tree-sitter-cpp (grammar limitation).
1005        // The patterns `qualified_defaulted_operator` and `qualified_deleted_operator` are
1006        // included to capture these scopes if tree-sitter-cpp ever parses them as
1007        // `function_definition` nodes.
1008        //
1009        // Test verifies correct behavior IF captured (future-proofing for grammar changes).
1010        let defaulted_assign_op = scopes
1011            .iter()
1012            .find(|s| s.name.contains("operator=") && s.scope_type == "method" && s.start_line > 6);
1013        let deleted_compare_op = scopes.iter().find(|s| {
1014            s.name.contains("operator==") && s.scope_type == "method" && s.start_line > 6
1015        });
1016
1017        if let Some(op) = defaulted_assign_op {
1018            assert_eq!(
1019                op.scope_type, "method",
1020                "Defaulted operator= should be method type"
1021            );
1022            assert!(
1023                op.name.contains("operator="),
1024                "Defaulted operator name should contain 'operator=', got: '{}'",
1025                op.name
1026            );
1027            eprintln!("Successfully captured out-of-class defaulted operator=");
1028        }
1029
1030        if let Some(op) = deleted_compare_op {
1031            assert_eq!(
1032                op.scope_type, "method",
1033                "Deleted operator== should be method type"
1034            );
1035            assert!(
1036                op.name.contains("operator=="),
1037                "Deleted operator name should contain 'operator==', got: '{}'",
1038                op.name
1039            );
1040            eprintln!("Successfully captured out-of-class deleted operator==");
1041        }
1042    }
1043}