1use 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
22pub 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 #[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 #[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 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 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 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 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 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 assert_eq!(
498 scopes.len(),
499 2,
500 "Expected 2 function scopes, got {}",
501 scopes.len()
502 );
503
504 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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}