1use std::collections::HashMap;
14use std::path::Path;
15
16use sqry_core::graph::unified::edge::kind::TypeOfContext;
17use sqry_core::graph::{
18 GraphBuilder, GraphResult, Language, Span,
19 unified::{GraphBuildHelper, NodeId as UnifiedNodeId, StagingGraph},
20};
21use tree_sitter::{Node, Tree};
22
23#[derive(Debug, Default)]
25pub struct PuppetGraphBuilder;
26
27impl PuppetGraphBuilder {
28 #[must_use]
30 pub fn new() -> Self {
31 Self
32 }
33}
34
35impl GraphBuilder for PuppetGraphBuilder {
36 fn build_graph(
37 &self,
38 tree: &Tree,
39 content: &[u8],
40 file: &Path,
41 staging: &mut StagingGraph,
42 ) -> GraphResult<()> {
43 let mut helper = GraphBuildHelper::new(staging, file, Language::Puppet);
45
46 let module_id = helper.add_module("<module>", None);
48
49 let mut class_ids = std::collections::HashMap::new();
51 collect_class_definitions(
52 tree.root_node(),
53 content,
54 module_id,
55 &mut helper,
56 &mut class_ids,
57 )?;
58
59 walk_ast_with_helper(
61 tree.root_node(),
62 content,
63 module_id,
64 &mut helper,
65 &class_ids,
66 )?;
67
68 Ok(())
69 }
70
71 fn language(&self) -> Language {
72 Language::Puppet
73 }
74}
75
76fn collect_class_definitions(
78 node: Node<'_>,
79 content: &[u8],
80 module_id: UnifiedNodeId,
81 helper: &mut GraphBuildHelper,
82 class_ids: &mut HashMap<String, UnifiedNodeId>,
83) -> GraphResult<()> {
84 match node.kind() {
85 "class_definition" | "defined_resource_type" => {
86 let mut cursor = node.walk();
88 for child in node.children(&mut cursor) {
89 if (child.kind() == "identifier" || child.kind() == "class_identifier")
90 && let Ok(name) = child.utf8_text(content)
91 {
92 let class_name = name.to_string();
93 let span = Some(span_from_node(node));
94
95 let class_id = helper.add_class(&class_name, span);
97 class_ids.insert(class_name.clone(), class_id);
98
99 helper.add_export_edge(module_id, class_id);
101
102 extract_parameter_types(node, content, class_id, &class_name, helper);
104 break;
105 }
106 }
107 }
108 _ => {}
109 }
110
111 let mut cursor = node.walk();
113 for child in node.children(&mut cursor) {
114 collect_class_definitions(child, content, module_id, helper, class_ids)?;
115 }
116
117 Ok(())
118}
119
120fn is_puppet_type_kind(kind: &str) -> bool {
122 matches!(
123 kind,
124 "builtin_type"
125 | "array_type"
126 | "composite_type"
127 | "attribute_type"
128 | "class_identifier"
129 | "identifier"
130 )
131}
132
133fn extract_parameter_types(
143 node: Node<'_>,
144 content: &[u8],
145 class_id: UnifiedNodeId,
146 class_name: &str,
147 helper: &mut GraphBuildHelper,
148) {
149 let mut cursor = node.walk();
151 for child in node.children(&mut cursor) {
152 if child.kind() == "parameter_list" {
153 let mut param_cursor = child.walk();
154 for param in child.children(&mut param_cursor) {
155 if param.kind() == "parameter" {
156 extract_single_param_type(param, content, class_id, class_name, helper);
157 }
158 }
159 }
160 }
161}
162
163fn extract_single_param_type(
181 param: Node<'_>,
182 content: &[u8],
183 class_id: UnifiedNodeId,
184 class_name: &str,
185 helper: &mut GraphBuildHelper,
186) {
187 let mut type_text = None;
188 let mut var_name = None;
189
190 let mut cursor = param.walk();
191 for child in param.children(&mut cursor) {
192 if type_text.is_none() && is_puppet_type_kind(child.kind()) && child.kind() != "identifier"
193 {
194 type_text = child.utf8_text(content).ok().map(ToString::to_string);
196 } else if child.kind() == "identifier" && type_text.is_none() && var_name.is_none() {
197 let maybe_type = child.utf8_text(content).ok().map(ToString::to_string);
200 if let Some(next) = child.next_named_sibling()
202 && next.kind() == "variable"
203 {
204 type_text = maybe_type;
205 }
206 } else if child.kind() == "variable" {
207 if let Ok(text) = child.utf8_text(content) {
209 var_name = Some(text.trim_start_matches('$').to_string());
210 }
211 }
212 }
213
214 if let (Some(type_str), Some(name)) = (type_text, var_name) {
216 let qualified_name = format!("{class_name}::{name}");
218 let param_id = helper.add_variable(&qualified_name, Some(span_from_node(param)));
219 let type_id = helper.add_type(&type_str, None);
220 helper.add_typeof_edge_with_context(
221 param_id,
222 type_id,
223 Some(TypeOfContext::Parameter),
224 None,
225 Some(&qualified_name),
226 );
227 helper.add_reference_edge(param_id, type_id);
228 helper.add_contains_edge(class_id, param_id);
229 }
230}
231
232fn walk_ast_with_helper(
234 node: Node<'_>,
235 content: &[u8],
236 module_id: UnifiedNodeId,
237 helper: &mut GraphBuildHelper,
238 class_ids: &HashMap<String, UnifiedNodeId>,
239) -> GraphResult<()> {
240 match node.kind() {
243 "include_statement" => {
244 extract_include_edge_with_helper(node, content, module_id, helper, "include");
245 }
246 "require_statement" => {
247 extract_include_edge_with_helper(node, content, module_id, helper, "require");
248 }
249 "class_definition" => {
250 extract_inheritance_edge_with_helper(node, content, helper, class_ids);
252 }
253 "resource_declaration" => {
254 extract_resource_call_with_helper(node, content, module_id, helper);
256 }
257 "function_call" => {
258 extract_function_call_with_helper(node, content, module_id, helper);
260 }
261 _ => {}
262 }
263
264 let mut cursor = node.walk();
266 for child in node.children(&mut cursor) {
267 walk_ast_with_helper(child, content, module_id, helper, class_ids)?;
268 }
269
270 Ok(())
271}
272
273fn extract_include_edge_with_helper(
284 node: Node<'_>,
285 content: &[u8],
286 module_id: UnifiedNodeId,
287 helper: &mut GraphBuildHelper,
288 _relation_type: &str,
289) {
290 let mut cursor = node.walk();
291
292 for child in node.children(&mut cursor) {
293 if (child.kind() == "identifier" || child.kind() == "class_identifier")
295 && let Ok(class_name) = child.utf8_text(content)
296 {
297 let class_path = class_name.replace("::", "/");
299 let qualified_name = format!("manifests/{class_path}.pp::{class_name}");
300
301 let target_id = helper.add_class(&qualified_name, Some(span_from_node(node)));
303
304 helper.add_import_edge(module_id, target_id);
306
307 return;
308 }
309
310 if child.kind() == "string"
312 && let Ok(text) = child.utf8_text(content)
313 {
314 let class_name = text.trim_matches('\'').trim_matches('"');
315 if !class_name.is_empty() {
316 let class_path = class_name.replace("::", "/");
318 let qualified_name = format!("manifests/{class_path}.pp::{class_name}");
319
320 let target_id = helper.add_class(&qualified_name, Some(span_from_node(node)));
322
323 helper.add_import_edge(module_id, target_id);
325
326 return;
327 }
328 }
329 }
330}
331
332fn extract_inheritance_edge_with_helper(
334 node: Node<'_>,
335 content: &[u8],
336 helper: &mut GraphBuildHelper,
337 class_ids: &HashMap<String, UnifiedNodeId>,
338) {
339 let mut cursor = node.walk();
340 let mut class_name = None;
341 let mut parent_name = None;
342
343 for child in node.children(&mut cursor) {
344 match child.kind() {
345 "identifier" | "class_identifier" => {
347 if class_name.is_none()
348 && let Ok(text) = child.utf8_text(content)
349 {
350 class_name = Some(text.to_string());
351 }
352 }
353 "class_inherits" => {
355 let mut inherits_cursor = child.walk();
356 for inherits_child in child.children(&mut inherits_cursor) {
357 if (inherits_child.kind() == "identifier"
358 || inherits_child.kind() == "class_identifier")
359 && let Ok(text) = inherits_child.utf8_text(content)
360 {
361 parent_name = Some(text.to_string());
362 }
363 }
364 }
365 _ => {}
366 }
367 }
368
369 if let (Some(class_name), Some(parent)) = (class_name, parent_name) {
371 let class_id = if let Some(&id) = class_ids.get(&class_name) {
373 id
374 } else {
375 helper.add_class(&class_name, Some(span_from_node(node)))
377 };
378
379 let parent_path = parent.replace("::", "/");
381 let parent_qualified = format!("manifests/{parent_path}.pp::{parent}");
382 let parent_id = helper.add_class(&parent_qualified, None);
383
384 helper.add_inherits_edge(class_id, parent_id);
386 }
387}
388
389fn extract_resource_call_with_helper(
395 node: Node<'_>,
396 content: &[u8],
397 module_id: UnifiedNodeId,
398 helper: &mut GraphBuildHelper,
399) {
400 let mut cursor = node.walk();
401
402 for child in node.children(&mut cursor) {
404 if child.kind() == "identifier"
405 && let Ok(resource_type) = child.utf8_text(content)
406 {
407 let callee_id =
409 helper.add_function(&format!("resource::{resource_type}"), None, false, false);
410
411 helper.add_call_edge_full_with_span(
413 module_id,
414 callee_id,
415 255,
416 false,
417 vec![span_from_node(node)],
418 );
419
420 return;
421 }
422 }
423}
424
425fn extract_function_call_with_helper(
432 node: Node<'_>,
433 content: &[u8],
434 module_id: UnifiedNodeId,
435 helper: &mut GraphBuildHelper,
436) {
437 let mut cursor = node.walk();
438
439 for child in node.children(&mut cursor) {
441 if child.kind() == "identifier"
442 && let Ok(function_name) = child.utf8_text(content)
443 {
444 let callee_id = helper.add_function(function_name, None, false, false);
446
447 helper.add_call_edge_full_with_span(
449 module_id,
450 callee_id,
451 255,
452 false,
453 vec![span_from_node(node)],
454 );
455
456 return;
457 }
458 }
459}
460
461fn span_from_node(node: Node<'_>) -> Span {
463 Span::from_bytes(node.start_byte(), node.end_byte())
464}
465
466#[cfg(test)]
467mod tests {
468 use super::*;
469 use sqry_core::graph::unified::build::test_helpers::*;
470 use sqry_core::graph::unified::node::NodeKind;
471 use std::path::PathBuf;
472
473 fn parse_puppet(source: &str) -> Tree {
474 let mut parser = tree_sitter::Parser::new();
475 parser
476 .set_language(&tree_sitter_puppet::LANGUAGE.into())
477 .unwrap();
478 parser.parse(source.as_bytes(), None).unwrap()
479 }
480
481 #[test]
482 fn test_extracts_include() {
483 let source = r"
484class myclass {
485 include other_class
486}
487";
488
489 let tree = parse_puppet(source);
490 let mut staging = StagingGraph::new();
491 let builder = PuppetGraphBuilder;
492 let file = PathBuf::from("manifests/myclass.pp");
493
494 builder
495 .build_graph(&tree, source.as_bytes(), &file, &mut staging)
496 .unwrap();
497
498 let import_edges = collect_import_edges(&staging);
499 assert_eq!(import_edges.len(), 1, "Should extract one include edge");
500
501 assert_has_node_with_kind(&staging, "myclass", NodeKind::Class);
503
504 assert_has_node(&staging, "other_class");
506 }
507
508 #[test]
509 fn test_extracts_require() {
510 let source = r"
511class myclass {
512 require base_class
513}
514";
515
516 let tree = parse_puppet(source);
517 let mut staging = StagingGraph::new();
518 let builder = PuppetGraphBuilder;
519 let file = PathBuf::from("manifests/myclass.pp");
520
521 builder
522 .build_graph(&tree, source.as_bytes(), &file, &mut staging)
523 .unwrap();
524
525 let import_edges = collect_import_edges(&staging);
526 assert_eq!(import_edges.len(), 1, "Should extract one require edge");
527
528 assert_has_node_with_kind(&staging, "myclass", NodeKind::Class);
530
531 assert_has_node(&staging, "base_class");
533 }
534
535 #[test]
536 fn test_extracts_string_class_name() {
537 let source = r"
538include 'mymodule::myclass'
539";
540
541 let tree = parse_puppet(source);
542 let mut staging = StagingGraph::new();
543 let builder = PuppetGraphBuilder;
544 let file = PathBuf::from("manifests/site.pp");
545
546 builder
547 .build_graph(&tree, source.as_bytes(), &file, &mut staging)
548 .unwrap();
549
550 let import_edges = collect_import_edges(&staging);
551 assert!(
552 !import_edges.is_empty(),
553 "Should extract include edge with string"
554 );
555
556 assert_has_node(&staging, "mymodule::myclass");
558 }
559
560 #[test]
561 fn test_multiple_includes() {
562 let source = r"
563class myclass {
564 include base
565 include networking
566 require security
567}
568";
569
570 let tree = parse_puppet(source);
571 let mut staging = StagingGraph::new();
572 let builder = PuppetGraphBuilder;
573 let file = PathBuf::from("manifests/myclass.pp");
574
575 builder
576 .build_graph(&tree, source.as_bytes(), &file, &mut staging)
577 .unwrap();
578
579 let import_edges = collect_import_edges(&staging);
580 assert_eq!(import_edges.len(), 3, "Should extract 3 import edges");
581
582 assert_has_node_with_kind(&staging, "myclass", NodeKind::Class);
584
585 assert_has_node(&staging, "base");
587 assert_has_node(&staging, "networking");
588 assert_has_node(&staging, "security");
589 }
590
591 #[test]
592 fn test_qualified_class_name() {
593 let source = r"
594include mymodule::submodule::myclass
595";
596
597 let tree = parse_puppet(source);
598 let mut staging = StagingGraph::new();
599 let builder = PuppetGraphBuilder;
600 let file = PathBuf::from("manifests/site.pp");
601
602 builder
603 .build_graph(&tree, source.as_bytes(), &file, &mut staging)
604 .unwrap();
605
606 let import_edges = collect_import_edges(&staging);
607 assert_eq!(import_edges.len(), 1, "Should extract one import edge");
608
609 assert_has_node(&staging, "mymodule");
611 }
612
613 #[test]
614 fn test_contain_not_supported_by_grammar() {
615 let source = r"
619class myclass {
620 contain contained_class
621}
622";
623
624 let tree = parse_puppet(source);
625 let mut staging = StagingGraph::new();
626 let builder = PuppetGraphBuilder;
627 let file = PathBuf::from("manifests/myclass.pp");
628
629 builder
630 .build_graph(&tree, source.as_bytes(), &file, &mut staging)
631 .unwrap();
632
633 let import_edges = collect_import_edges(&staging);
634 assert_eq!(import_edges.len(), 0, "Contain not supported by grammar");
637
638 assert_has_node_with_kind(&staging, "myclass", NodeKind::Class);
640 }
641
642 #[test]
643 fn test_extracts_class_inheritance() {
644 let source = r"
645class myclass inherits parent_class {
646 # class body
647}
648";
649
650 let tree = parse_puppet(source);
651 let mut staging = StagingGraph::new();
652 let builder = PuppetGraphBuilder;
653 let file = PathBuf::from("manifests/myclass.pp");
654
655 builder
656 .build_graph(&tree, source.as_bytes(), &file, &mut staging)
657 .unwrap();
658
659 let inherits_edges = collect_inherits_edges(&staging);
660 assert_eq!(inherits_edges.len(), 1, "Should extract one inherits edge");
661
662 assert_has_node_with_kind(&staging, "myclass", NodeKind::Class);
664 assert_has_node(&staging, "parent_class");
665
666 assert_has_inherits_edge(
668 &staging,
669 "myclass",
670 "manifests/parent_class.pp::parent_class",
671 );
672 }
673
674 #[test]
675 fn test_class_without_inheritance() {
676 let source = r"
677class myclass {
678 # class body without inheritance
679}
680";
681
682 let tree = parse_puppet(source);
683 let mut staging = StagingGraph::new();
684 let builder = PuppetGraphBuilder;
685 let file = PathBuf::from("manifests/myclass.pp");
686
687 builder
688 .build_graph(&tree, source.as_bytes(), &file, &mut staging)
689 .unwrap();
690
691 let inherits_edges = collect_inherits_edges(&staging);
692 assert_eq!(inherits_edges.len(), 0, "Should have no inherits edges");
693
694 assert_has_node_with_kind(&staging, "myclass", NodeKind::Class);
696 }
697
698 #[test]
699 fn test_empty_file() {
700 let source = "";
701
702 let tree = parse_puppet(source);
703 let mut staging = StagingGraph::new();
704 let builder = PuppetGraphBuilder;
705 let file = PathBuf::from("manifests/empty.pp");
706
707 builder
708 .build_graph(&tree, source.as_bytes(), &file, &mut staging)
709 .unwrap();
710
711 assert_eq!(
713 count_nodes_by_kind(&staging, NodeKind::Module),
714 1,
715 "Empty file should have exactly one module node"
716 );
717
718 assert_eq!(
720 count_nodes_by_kind(&staging, NodeKind::Class),
721 0,
722 "Empty file should have no class nodes"
723 );
724 }
725
726 #[test]
727 fn test_mixed_statements() {
728 let source = r"
730class myclass inherits base_class {
731 include helper
732 require dependency
733}
734";
735
736 let tree = parse_puppet(source);
737 let mut staging = StagingGraph::new();
738 let builder = PuppetGraphBuilder;
739 let file = PathBuf::from("manifests/myclass.pp");
740
741 builder
742 .build_graph(&tree, source.as_bytes(), &file, &mut staging)
743 .unwrap();
744
745 let inherits_edges = collect_inherits_edges(&staging);
747 assert_eq!(inherits_edges.len(), 1, "Should have one inherits edge");
748
749 let import_edges = collect_import_edges(&staging);
751 assert_eq!(
752 import_edges.len(),
753 2,
754 "Should have two import edges (include + require)"
755 );
756
757 assert_has_node_with_kind(&staging, "myclass", NodeKind::Class);
759 assert_has_node(&staging, "base_class");
760 assert_has_node(&staging, "helper");
761 assert_has_node(&staging, "dependency");
762
763 assert_has_inherits_edge(&staging, "myclass", "manifests/base_class.pp::base_class");
765 }
766
767 #[test]
768 fn test_nested_class_include() {
769 let source = r"
770class outer {
771 class { 'inner':
772 # nested resource-style class
773 }
774 include nested_include
775}
776";
777
778 let tree = parse_puppet(source);
779 let mut staging = StagingGraph::new();
780 let builder = PuppetGraphBuilder;
781 let file = PathBuf::from("manifests/outer.pp");
782
783 builder
784 .build_graph(&tree, source.as_bytes(), &file, &mut staging)
785 .unwrap();
786
787 let import_edges = collect_import_edges(&staging);
788 assert!(
789 !import_edges.is_empty(),
790 "Should extract include edge from nested class"
791 );
792
793 assert_has_node_with_kind(&staging, "outer", NodeKind::Class);
795 assert_has_node(&staging, "nested_include");
796 }
797}
798
799#[cfg(test)]
801mod active_tests {
802 use super::*;
803 use sqry_core::graph::unified::build::StagingOp;
804 use sqry_core::graph::unified::edge::EdgeKind as UnifiedEdgeKind;
805 use std::path::PathBuf;
806
807 fn parse_puppet(source: &str) -> Tree {
808 let mut parser = tree_sitter::Parser::new();
809 parser
810 .set_language(&tree_sitter_puppet::LANGUAGE.into())
811 .unwrap();
812 parser.parse(source.as_bytes(), None).unwrap()
813 }
814
815 fn extract_import_edges(staging: &StagingGraph) -> Vec<&UnifiedEdgeKind> {
817 staging
818 .operations()
819 .iter()
820 .filter_map(|op| {
821 if let StagingOp::AddEdge { kind, .. } = op
822 && matches!(kind, UnifiedEdgeKind::Imports { .. })
823 {
824 return Some(kind);
825 }
826 None
827 })
828 .collect()
829 }
830
831 fn extract_call_edges(staging: &StagingGraph) -> Vec<&UnifiedEdgeKind> {
833 staging
834 .operations()
835 .iter()
836 .filter_map(|op| {
837 if let StagingOp::AddEdge { kind, .. } = op
838 && matches!(kind, UnifiedEdgeKind::Calls { .. })
839 {
840 return Some(kind);
841 }
842 None
843 })
844 .collect()
845 }
846
847 fn extract_inherits_edges(staging: &StagingGraph) -> Vec<&UnifiedEdgeKind> {
849 staging
850 .operations()
851 .iter()
852 .filter_map(|op| {
853 if let StagingOp::AddEdge { kind, .. } = op
854 && matches!(kind, UnifiedEdgeKind::Inherits)
855 {
856 return Some(kind);
857 }
858 None
859 })
860 .collect()
861 }
862
863 #[test]
864 fn test_puppet_graph_builder_language() {
865 let builder = PuppetGraphBuilder;
866 assert_eq!(builder.language(), Language::Puppet);
867 }
868
869 #[test]
870 fn test_extracts_resource_declaration_calls() {
871 let source = r"
872class myapp {
873 package { 'nginx':
874 ensure => installed,
875 }
876
877 service { 'nginx':
878 ensure => running,
879 }
880
881 file { '/etc/nginx/nginx.conf':
882 ensure => present,
883 }
884}
885";
886
887 let tree = parse_puppet(source);
888 let mut staging = StagingGraph::new();
889 let builder = PuppetGraphBuilder;
890 let file = PathBuf::from("manifests/myapp.pp");
891
892 builder
893 .build_graph(&tree, source.as_bytes(), &file, &mut staging)
894 .expect("build_graph should succeed");
895
896 let call_edges = extract_call_edges(&staging);
897 assert_eq!(
898 call_edges.len(),
899 3,
900 "Should extract three call edges for resource declarations (package, service, file)"
901 );
902 }
903
904 #[test]
905 fn test_extracts_function_calls() {
906 let source = r#"
907define myapp::config($port = 80) {
908 file { "/etc/myapp/${name}.conf":
909 content => template('myapp/config.erb'),
910 }
911}
912"#;
913
914 let tree = parse_puppet(source);
915 let mut staging = StagingGraph::new();
916 let builder = PuppetGraphBuilder;
917 let file = PathBuf::from("manifests/myapp/config.pp");
918
919 builder
920 .build_graph(&tree, source.as_bytes(), &file, &mut staging)
921 .expect("build_graph should succeed");
922
923 let call_edges = extract_call_edges(&staging);
924 assert!(
926 call_edges.len() >= 2,
927 "Should extract call edges for file resource and template() function"
928 );
929 }
930
931 #[test]
932 fn test_extracts_include_import_edge() {
933 let source = r"
934class webserver {
935 include myapp
936}
937";
938
939 let tree = parse_puppet(source);
940 let mut staging = StagingGraph::new();
941 let builder = PuppetGraphBuilder;
942 let file = PathBuf::from("manifests/webserver.pp");
943
944 builder
945 .build_graph(&tree, source.as_bytes(), &file, &mut staging)
946 .expect("build_graph should succeed");
947
948 let import_edges = extract_import_edges(&staging);
949 assert_eq!(
950 import_edges.len(),
951 1,
952 "Should extract one import edge for include statement"
953 );
954 }
955
956 #[test]
957 fn test_extracts_require_import_edge() {
958 let source = r"
959class webserver {
960 require myapp::prereqs
961}
962";
963
964 let tree = parse_puppet(source);
965 let mut staging = StagingGraph::new();
966 let builder = PuppetGraphBuilder;
967 let file = PathBuf::from("manifests/webserver.pp");
968
969 builder
970 .build_graph(&tree, source.as_bytes(), &file, &mut staging)
971 .expect("build_graph should succeed");
972
973 let import_edges = extract_import_edges(&staging);
974 assert_eq!(
975 import_edges.len(),
976 1,
977 "Should extract one import edge for require statement"
978 );
979 }
980
981 #[test]
982 fn test_extracts_inheritance_edge() {
983 let source = r"
984class child inherits parent {
985 # child class body
986}
987";
988
989 let tree = parse_puppet(source);
990 let mut staging = StagingGraph::new();
991 let builder = PuppetGraphBuilder;
992 let file = PathBuf::from("manifests/child.pp");
993
994 builder
995 .build_graph(&tree, source.as_bytes(), &file, &mut staging)
996 .expect("build_graph should succeed");
997
998 let inherits_edges = extract_inherits_edges(&staging);
999 assert_eq!(
1000 inherits_edges.len(),
1001 1,
1002 "Should extract one inherits edge for class inheritance"
1003 );
1004 }
1005
1006 #[test]
1007 fn test_empty_file_no_edges() {
1008 let source = "";
1009
1010 let tree = parse_puppet(source);
1011 let mut staging = StagingGraph::new();
1012 let builder = PuppetGraphBuilder;
1013 let file = PathBuf::from("manifests/empty.pp");
1014
1015 builder
1016 .build_graph(&tree, source.as_bytes(), &file, &mut staging)
1017 .expect("build_graph should succeed");
1018
1019 let call_edges = extract_call_edges(&staging);
1020 assert!(
1021 call_edges.is_empty(),
1022 "Empty file should have no call edges"
1023 );
1024
1025 let import_edges = extract_import_edges(&staging);
1026 assert!(
1027 import_edges.is_empty(),
1028 "Empty file should have no import edges"
1029 );
1030 }
1031
1032 #[test]
1033 fn test_mixed_statements() {
1034 let source = r"
1035class myapp inherits base {
1036 include helper
1037 require prereqs
1038
1039 package { 'app':
1040 ensure => installed,
1041 }
1042
1043 $config = lookup('app::config')
1044}
1045";
1046
1047 let tree = parse_puppet(source);
1048 let mut staging = StagingGraph::new();
1049 let builder = PuppetGraphBuilder;
1050 let file = PathBuf::from("manifests/myapp.pp");
1051
1052 builder
1053 .build_graph(&tree, source.as_bytes(), &file, &mut staging)
1054 .expect("build_graph should succeed");
1055
1056 let import_edges = extract_import_edges(&staging);
1058 assert_eq!(
1059 import_edges.len(),
1060 2,
1061 "Should extract two import edges (include + require)"
1062 );
1063
1064 let inherits_edges = extract_inherits_edges(&staging);
1066 assert_eq!(inherits_edges.len(), 1, "Should have one inherits edge");
1067
1068 let call_edges = extract_call_edges(&staging);
1070 assert!(
1071 call_edges.len() >= 2,
1072 "Should have at least 2 call edges (package resource + lookup function)"
1073 );
1074 }
1075
1076 #[test]
1077 fn test_node_definition_with_resources() {
1078 let source = r"
1079node 'web01.example.com' {
1080 package { 'nginx':
1081 ensure => installed,
1082 }
1083}
1084";
1085
1086 let tree = parse_puppet(source);
1087 let mut staging = StagingGraph::new();
1088 let builder = PuppetGraphBuilder;
1089 let file = PathBuf::from("manifests/nodes.pp");
1090
1091 builder
1092 .build_graph(&tree, source.as_bytes(), &file, &mut staging)
1093 .expect("build_graph should succeed");
1094
1095 let call_edges = extract_call_edges(&staging);
1096 assert_eq!(
1097 call_edges.len(),
1098 1,
1099 "Should extract one call edge for package resource"
1100 );
1101 }
1102
1103 fn extract_typeof_edges(staging: &StagingGraph) -> Vec<&UnifiedEdgeKind> {
1105 staging
1106 .operations()
1107 .iter()
1108 .filter_map(|op| {
1109 if let StagingOp::AddEdge { kind, .. } = op
1110 && matches!(kind, UnifiedEdgeKind::TypeOf { .. })
1111 {
1112 return Some(kind);
1113 }
1114 None
1115 })
1116 .collect()
1117 }
1118
1119 #[test]
1120 fn test_class_typed_params_create_typeof_edges() {
1121 let source = r"
1122class myapp (
1123 String $pkg,
1124 Integer $port = 80,
1125) {
1126 # class body
1127}
1128";
1129
1130 let tree = parse_puppet(source);
1131 let mut staging = StagingGraph::new();
1132 let builder = PuppetGraphBuilder;
1133 let file = PathBuf::from("manifests/myapp.pp");
1134
1135 builder
1136 .build_graph(&tree, source.as_bytes(), &file, &mut staging)
1137 .expect("build_graph should succeed");
1138
1139 let typeof_edges = extract_typeof_edges(&staging);
1140 assert_eq!(
1141 typeof_edges.len(),
1142 2,
1143 "Class with 2 typed parameters should create 2 TypeOf edges"
1144 );
1145 }
1146
1147 #[test]
1148 fn test_define_typed_params_create_typeof_edges() {
1149 let source = r"
1150define myapp::config (
1151 String $name,
1152 Boolean $ssl = false,
1153) {
1154 # define body
1155}
1156";
1157
1158 let tree = parse_puppet(source);
1159 let mut staging = StagingGraph::new();
1160 let builder = PuppetGraphBuilder;
1161 let file = PathBuf::from("manifests/myapp/config.pp");
1162
1163 builder
1164 .build_graph(&tree, source.as_bytes(), &file, &mut staging)
1165 .expect("build_graph should succeed");
1166
1167 let typeof_edges = extract_typeof_edges(&staging);
1168 assert_eq!(
1169 typeof_edges.len(),
1170 2,
1171 "Define with 2 typed parameters should create 2 TypeOf edges"
1172 );
1173 }
1174
1175 #[test]
1176 fn test_class_untyped_params_no_typeof_edges() {
1177 let source = r"
1178class myapp (
1179 $pkg = 'nginx',
1180 $port = 80,
1181) {
1182 # class body
1183}
1184";
1185
1186 let tree = parse_puppet(source);
1187 let mut staging = StagingGraph::new();
1188 let builder = PuppetGraphBuilder;
1189 let file = PathBuf::from("manifests/myapp.pp");
1190
1191 builder
1192 .build_graph(&tree, source.as_bytes(), &file, &mut staging)
1193 .expect("build_graph should succeed");
1194
1195 let typeof_edges = extract_typeof_edges(&staging);
1196 assert!(
1197 typeof_edges.is_empty(),
1198 "Class with only untyped parameters should have no TypeOf edges"
1199 );
1200 }
1201
1202 #[test]
1203 fn test_complex_puppet_types_create_typeof_edges() {
1204 let source = r"
1205class myapp (
1206 Array[String] $pkgs,
1207 Hash $config = {},
1208) {
1209 # class body
1210}
1211";
1212
1213 let tree = parse_puppet(source);
1214 let mut staging = StagingGraph::new();
1215 let builder = PuppetGraphBuilder;
1216 let file = PathBuf::from("manifests/myapp.pp");
1217
1218 builder
1219 .build_graph(&tree, source.as_bytes(), &file, &mut staging)
1220 .expect("build_graph should succeed");
1221
1222 let typeof_edges = extract_typeof_edges(&staging);
1223 assert_eq!(
1224 typeof_edges.len(),
1225 2,
1226 "Class with 2 complex typed parameters should create 2 TypeOf edges"
1227 );
1228 }
1229
1230 #[test]
1231 fn test_mixed_typed_untyped_params() {
1232 let source = r"
1233class myapp (
1234 String $name,
1235 $untyped_param,
1236 Integer $port = 80,
1237) {
1238 # class body
1239}
1240";
1241
1242 let tree = parse_puppet(source);
1243 let mut staging = StagingGraph::new();
1244 let builder = PuppetGraphBuilder;
1245 let file = PathBuf::from("manifests/myapp.pp");
1246
1247 builder
1248 .build_graph(&tree, source.as_bytes(), &file, &mut staging)
1249 .expect("build_graph should succeed");
1250
1251 let typeof_edges = extract_typeof_edges(&staging);
1252 assert_eq!(
1253 typeof_edges.len(),
1254 2,
1255 "Class with 2 typed + 1 untyped should create 2 TypeOf edges"
1256 );
1257 }
1258
1259 #[test]
1260 fn test_class_without_params_no_typeof_edges() {
1261 let source = r"
1262class myapp {
1263 package { 'nginx':
1264 ensure => installed,
1265 }
1266}
1267";
1268
1269 let tree = parse_puppet(source);
1270 let mut staging = StagingGraph::new();
1271 let builder = PuppetGraphBuilder;
1272 let file = PathBuf::from("manifests/myapp.pp");
1273
1274 builder
1275 .build_graph(&tree, source.as_bytes(), &file, &mut staging)
1276 .expect("build_graph should succeed");
1277
1278 let typeof_edges = extract_typeof_edges(&staging);
1279 assert!(
1280 typeof_edges.is_empty(),
1281 "Class without parameters should have no TypeOf edges"
1282 );
1283 }
1284
1285 #[test]
1286 #[allow(clippy::items_after_statements)] fn test_same_param_name_different_classes_distinct_nodes() {
1288 let source = r"
1291class foo (
1292 String $name = 'a',
1293) { }
1294
1295class bar (
1296 Integer $name = 1,
1297) { }
1298";
1299
1300 let tree = parse_puppet(source);
1301 let mut staging = StagingGraph::new();
1302 let builder = PuppetGraphBuilder;
1303 let file = PathBuf::from("manifests/classes.pp");
1304
1305 builder
1306 .build_graph(&tree, source.as_bytes(), &file, &mut staging)
1307 .expect("build_graph should succeed");
1308
1309 let typeof_edges = extract_typeof_edges(&staging);
1310 assert_eq!(
1311 typeof_edges.len(),
1312 2,
1313 "Two classes with typed params should create 2 TypeOf edges"
1314 );
1315
1316 use sqry_core::graph::unified::build::test_helpers::assert_has_node_with_kind;
1318 use sqry_core::graph::unified::node::NodeKind;
1319 assert_has_node_with_kind(&staging, "foo::name", NodeKind::Variable);
1320 assert_has_node_with_kind(&staging, "bar::name", NodeKind::Variable);
1321
1322 assert_has_node_with_kind(&staging, "String", NodeKind::Type);
1324 assert_has_node_with_kind(&staging, "Integer", NodeKind::Type);
1325 }
1326
1327 #[test]
1328 #[allow(clippy::items_after_statements)] fn test_custom_namespaced_type_extracts_correctly() {
1330 let source = r"
1333class myapp (
1334 String $name,
1335 Stdlib::Absolutepath $config_dir = '/etc/myapp',
1336) {
1337 # class body
1338}
1339";
1340
1341 let tree = parse_puppet(source);
1342 let mut staging = StagingGraph::new();
1343 let builder = PuppetGraphBuilder;
1344 let file = PathBuf::from("manifests/myapp.pp");
1345
1346 builder
1347 .build_graph(&tree, source.as_bytes(), &file, &mut staging)
1348 .expect("build_graph should succeed");
1349
1350 let typeof_edges = extract_typeof_edges(&staging);
1351 assert_eq!(
1352 typeof_edges.len(),
1353 2,
1354 "Should have 2 TypeOf edges (String + Stdlib::Absolutepath)"
1355 );
1356
1357 use sqry_core::graph::unified::build::test_helpers::assert_has_node_with_kind;
1359 use sqry_core::graph::unified::node::NodeKind;
1360 assert_has_node_with_kind(&staging, "Stdlib::Absolutepath", NodeKind::Type);
1361 assert_has_node_with_kind(&staging, "String", NodeKind::Type);
1362 assert_has_node_with_kind(&staging, "myapp::config_dir", NodeKind::Variable);
1363 }
1364}
1365